Skip to content

Order with Line Items

An order with any number of lines, each with a product, a quantity and a unit price, and a total worked out on the server.

models.py
from datetime import date
from decimal import Decimal

from pydantic import computed_field

from pydantic_schemaforms import Field, FormModel


class LineItem(FormModel):
    product: str = Field(title='Product', min_length=2)
    quantity: int = Field(1, title='Quantity', ge=1, le=999)
    unit_price: Decimal = Field(title='Unit price', ge=0, decimal_places=2, ui_element='currency')

    @computed_field
    @property
    def total(self) -> Decimal:
        return self.quantity * self.unit_price


class OrderForm(FormModel):
    customer: str = Field(title='Customer', ui_autofocus=True)
    delivery_date: date = Field(title='Delivery date')
    items: list[LineItem] = Field(
        default_factory=list,
        title='Line items',
        min_length=1,
        max_length=50,
        ui_add_button_label='Add a line',
        ui_item_title_template='{product}',
    )
    notes: str = Field('', title='Notes', ui_element='textarea')
main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import OrderForm

from pydantic_schemaforms import html, render_form_html_async

app = FastAPI()
FORM_OPTIONS = {'submit_url': '/orders/new', 'include_framework_assets': True, 'asset_mode': 'cdn'}


@app.get('/orders/new', response_class=HTMLResponse)
async def new_order():
    return await render_form_html_async(OrderForm, **FORM_OPTIONS)


@app.post('/orders/new', response_class=HTMLResponse)
async def create_order(request: Request):
    result = OrderForm.validate(await request.form(), **FORM_OPTIONS)
    if not result.is_valid:
        return await result.render_with_errors_async()
    order = result.instance
    lines = len(order.items)
    total = sum(item.total for item in order.items)
    return html(t'<p>Order for {order.customer}: {lines} lines, {total:.2f} in total.</p>')
main.py
from flask import Flask, request
from models import OrderForm

from pydantic_schemaforms import html, render_form_html

app = Flask(__name__)
FORM_OPTIONS = {'submit_url': '/orders/new', 'include_framework_assets': True, 'asset_mode': 'cdn'}


@app.get('/orders/new')
def new_order():
    return render_form_html(OrderForm, **FORM_OPTIONS)


@app.post('/orders/new')
def create_order():
    result = OrderForm.validate(request.form, **FORM_OPTIONS)
    if not result.is_valid:
        return result.render_with_errors()
    order = result.instance
    lines = len(order.items)
    total = sum(item.total for item in order.items)
    return html(t'<p>Order for {order.customer}: {lines} lines, {total:.2f} in total.</p>')

How it works

  • list[LineItem] is a repeating sub-form with an "Add a line" button. min_length=1 means the form starts with one empty line and can't be sent with none; max_length=50 caps it.
  • ui_item_title_template="{product}" titles each line's card with its product as the user types it.
  • ui_element="currency" shows the price as $1,012.50 while the user types. The symbol and separators are removed before validation, so the field is still a Decimal.
  • @computed_field total isn't an input: it's worked out from the validated line, so the route never trusts a total sent by the browser.
  • Errors name the line. A bad price on the second line shows under that input, and in the summary as "Item #2 — Unit Price: ...".