Skip to content

intent: generates items: has no source-row rule - every member row is cloned into a line, so an empty or unapproved EmployeeTimesheet is billed (or blocks the whole month) #7091

Description

@delchev

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions