Repository navigation
Conversation
… so aging can span invoices, notes and opening items (#7946) Cause: a report had exactly one source. An aged receivables / payables report by counterparty must add up to the receivable balance, whose open items live in several entities (invoices, debit notes, credit notes, migrated opening items), and a revenue report must net credit notes against invoices - neither was expressible, and #7888's aging: named the missing union as a limit. Change: union: [{ source, model?, counterparty?, date?, due?, amount, filter? }] replaces source:. Each member maps its own fields onto the logical columns source / counterparty / date / due / amount (signed, the aging.amount grammar, which now also takes one leading sign: "-total"). ReportUnion builds a logical entity of those columns, shared by the parser (dimensions, measures, parameters validated against it) and the generator (resolved against it), so a union report is an ordinary report over that entity. Its table is a generated parameter-free <TABLE>_UNION view (<Report>Union.view): one UNION ALL arm per member, carrying the member's filter and its own lifecycle scope, dates cast to DATE, amount terms COALESCEd. With aging: the members supply due, amount and date. A model: member is read from its owner's table and checked against its resolved .model at generation. counterparty/date/due are mapped by every member or none; source + union, a ledger kind, and aging naming due/amount/date over a union are refused at parse. Verified: UnionReportTest runs the view and the report on H2 over invoices + credit notes + opening items (buckets per counterparty add up to the open amount, draft excluded per member, member filter, opening item aged by its own timestamp due date, netted revenue by source, cross-model arm SQL, parse refusals); engine-intent unit suite (2109) green; IntentEmissionCoverageIT green with a published EntryAgingUnion read over REST with and without ?asOf=; formatter:validate with the cache wiped; release-profile javadoc on engine-intent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cause
A report had exactly one
source. An aged receivables / payables report by counterparty must add up to the receivable (payable) balance, whose open items live in several entities: invoices, debit notes, credit notes (negative) and migrated opening items. A revenue report must net credit notes against invoices. Neither could be expressed, and #7888'saging:explicitly named the missing union as a limit.Change
union:replacessource:. Each member maps its own fields onto the logical columnssource(the member's entity name),counterparty(a to-one relation),date,dueand a signedamount.amountuses theaging.amountgrammar, which now also accepts one leading sign ("-total"), so a credit note can be negated.dimensions: [source], measures: ["sum(amount)"]over invoicetotaland credit note-totalgives net revenue.ReportUnionbuilds one logical entity from those columns. It is shared by the parser (dimensions, measures, parameters and period are validated against it) and the generator (they are resolved against it). So a union report is an ordinary report over that entity, and the rest of the report path does not know about unions.<TABLE>_UNION(<Report>Union.view), following the existing_LINES/_CORRESPONDENCEpattern. It has oneUNION ALLarm per member. Each arm carries the member'sfilterand its own lifecycle scope, so a draft of any document type is excluded. Dates are cast toDATE, so a timestamp member and a date member share a column. Amount terms areCOALESCEd.aging:, the members supply the due date, amount and document date, and the block carries onlybuckets/asOf. The as-of date stays on the.report.model:) is read from its owner's table. Its fields, counterparty target and status stages are checked against the owner's resolved.modelat generation.sourcetogether withunion;kind;counterparty,dateorduemapped by some members but not all;agingnamingdue/amount/dateover a union, or a union whose members don't mapdue.drillon a union report, since a row counts several entities.CLAUDE.md(the reports: aging buckets - open items bucketed by days past due as of a report date (not yet due / 0-30 / 31-60 / 61-90 / 90+) #7888 entry's "NOT done: union" now points here),intent-dsl-features.md.Verification
UnionReportTestruns the generated view and report on H2 over invoices, credit notes and opening items. It checks:It also checks netted revenue by
source, the cross-model arm's SQL, and every parse refusal.engine-intentunit suite (2109 tests) is green, includingAgingReportTestandReportEditorRoundTripTestunchanged.IntentEmissionCoverageITis green. It publishesEntryAgingUnion(debit and negated credit as two members of one union), which synchronizes the generated view, compiles the controller and reads it over REST with and without?asOf=.formatter:validatepasses with the cache wiped, and the release-profile javadoc build ofengine-intentpasses.CAST(... AS DATE),CAST('x' AS VARCHAR(255)),COALESCEandUNION ALL, but I did not run it on PG locally. The CI PG leg covers the IT..model: the unit test covers the convention fallback only.Fixes #7946
🤖 Generated with Claude Code