What happens
A generates: items: block clones EVERY source item row into a target line. There is no way to say which source rows qualify. base-timesheets invoice-from-timesheet (items: { from: EmployeeTimesheet, to: SalesInvoiceItem, map: { quantity: totalHours, price: rate, name: employeeName } }) therefore bills every EmployeeTimesheet of the project-month:
GeneratesItemsIntent carries from, to, map, defaults - nothing else (checked on master at 7c275b4). checks: lives on entities (exactlyOne / itemsSumEqual / itemsMin) and cannot gate a generate's item selection either.
Expected
An authored rule on the items: block, in the shape the DSL already uses elsewhere (schedules[].where, transitions.from), for example:
items:
from: EmployeeTimesheet
to: SalesInvoiceItem
where:
- { field: Status, op: eq, value: 3 } # only APPROVED member timesheets become lines
- { field: totalHours, op: gt, value: 0 } # an empty one is not a line
map: { name: employeeName, quantity: totalHours, price: rate }
with a refuse: variant (or a checks: kind on the generate) for the cases where an unqualified row should stop the whole generate with an authored message rather than be skipped ("Member timesheet [n] is not approved"), since silently dropping a REJECTED timesheet from the invoice and silently billing it are both wrong for different months. A generate whose where filters out every item should refuse (an invoice with no lines is the QG-2 class), not create a header-only document.
Why it matters
"Invoice the approved month" is the one flow a billing clerk runs; today the module can either bill unapproved hours or not bill at all. Fleet-wide: every items: generate (proforma -> invoice, quotation -> order, order -> invoice) has the same gap.
Interim in the fleet (base-timesheets #19): rely on #7081's refusal-by-name and delete/fill the empty timesheet; the status gap stays open.
Found 2026-09-06 on sta (14.46.0 train), end-user walk through the Billing/Timesheets lenses (businessintents-catalog/scenarios/); reported as BusinessIntents/base-timesheets#19. Related: #7069 (atomicity, fixed by #7081), #7068 (source status guard, fixed by #7080), #7070 (schedule natural key, fixed by #7079).
What happens
A
generates:items:block clones EVERY source item row into a target line. There is no way to say which source rows qualify. base-timesheetsinvoice-from-timesheet(items: { from: EmployeeTimesheet, to: SalesInvoiceItem, map: { quantity: totalHours, price: rate, name: employeeName } }) therefore bills every EmployeeTimesheet of the project-month:totalHoursnull) - since intent: a create-from is one transaction, and a missing required value is refused by name (#7069) #7081 the whole Generate is refused 400SalesInvoiceItem.Quantity is required (from EmployeeTimesheet [7])and nothing is committed (correct, and far better than the header-only invoice of intent: generates is not atomic - when an items: insert fails the header document and the sourceStatus flip stay committed (HTTP 500, header-only invoice, source marked INVOICED) #7069), but the clerk cannot invoice the month at all until that empty timesheet is deleted by hand; the intent cannot say "skip it";GeneratesItemsIntentcarriesfrom,to,map,defaults- nothing else (checked on master at 7c275b4).checks:lives on entities (exactlyOne/itemsSumEqual/itemsMin) and cannot gate a generate's item selection either.Expected
An authored rule on the
items:block, in the shape the DSL already uses elsewhere (schedules[].where,transitions.from), for example:with a
refuse:variant (or achecks:kind on the generate) for the cases where an unqualified row should stop the whole generate with an authored message rather than be skipped ("Member timesheet [n] is not approved"), since silently dropping a REJECTED timesheet from the invoice and silently billing it are both wrong for different months. A generate whosewherefilters out every item should refuse (an invoice with no lines is the QG-2 class), not create a header-only document.Why it matters
"Invoice the approved month" is the one flow a billing clerk runs; today the module can either bill unapproved hours or not bill at all. Fleet-wide: every
items:generate (proforma -> invoice, quotation -> order, order -> invoice) has the same gap.Interim in the fleet (base-timesheets #19): rely on #7081's refusal-by-name and delete/fill the empty timesheet; the status gap stays open.
Found 2026-09-06 on sta (14.46.0 train), end-user walk through the Billing/Timesheets lenses (
businessintents-catalog/scenarios/); reported as BusinessIntents/base-timesheets#19. Related: #7069 (atomicity, fixed by #7081), #7068 (source status guard, fixed by #7080), #7070 (schedule natural key, fixed by #7079).