Module: Ksef::FA3::Summaries
- Included in:
- Invoice
- Defined in:
- lib/ksef/fa3/summaries.rb
Overview
The summary arithmetic of an invoice: what each rate code contributes, and the three totals the document carries.
Extracted from Invoice on 2026-08-26, when tier 3 arrived and gave these methods a second caller. Two reasons beyond the class-length gate. They are one coherent subject — every one of them answers "what does this invoice come to?" — and BusinessValidator reconciles against them, so it helps to have somewhere to point at rather than a span of a 300-line class.
The :per_line / :per_summary split lives here too. Both are permitted by Polish VAT
law and they differ by a grosz, which is why the strategy is an explicit field rather
than a choice this gem makes quietly (DESIGN.md §7.3).
Class Method Summary collapse
-
.derived_gross(lines, rounding) ⇒ BigDecimal
What #gross_total would answer with nothing stated.
-
.net_by_rate(lines) ⇒ Object
The same arithmetic as the instance methods, over a bare line list — so Invoice#initialize can ask "what would this invoice derive?" before there is an invoice to ask.
-
.per_line(lines) ⇒ Object
Round each line, then sum.
-
.per_summary(nets) ⇒ Object
Sum the nets, then round once.
-
.summable(lines) ⇒ Object
StanPrzedrows are excluded, and that is a correctness fix rather than a tidy. A row marked "stan przed korektą" states the position as it was; a correction shows it beside its replacement. - .vat_by_rate(lines, rounding) ⇒ Object
Instance Method Summary collapse
-
#gross_total ⇒ Object
A stated
P_15wins over a derived one — it is what the document says is owed, and bucket-level rounding means the derivation can be a grosz out (Invoice.scaled_gross). -
#net_by_rate ⇒ Hash{String => BigDecimal}
This is what the lines say, always — Invoice#totals does not enter into it, because a stated summary is keyed by bucket and cannot be resolved back to rate codes (§8.1a).
-
#net_total ⇒ Object
The three figures the document actually carries: read from Invoice#totals when the invoice states them, computed from the lines otherwise.
- #vat_by_rate ⇒ Hash{String => BigDecimal}
- #vat_total ⇒ Object
Class Method Details
.derived_gross(lines, rounding) ⇒ BigDecimal
Returns what #gross_total would answer with nothing stated.
57 58 59 60 |
# File 'lib/ksef/fa3/summaries.rb', line 57 def derived_gross(lines, rounding) net_by_rate(lines).values.sum(BigDecimal(0)) + vat_by_rate(lines, rounding).values.sum(BigDecimal(0)) end |
.net_by_rate(lines) ⇒ Object
The same arithmetic as the instance methods, over a bare line list — so
Invoice#initialize can ask "what would this invoice derive?" before there is an
invoice to ask. That is what lets stated_gross canonicalise in the model,
where Invoice.positioned canonicalises row_number, rather than only in the
parser (docs/REFERENCE.md §17.2).
24 25 26 27 28 |
# File 'lib/ksef/fa3/summaries.rb', line 24 def net_by_rate(lines) summable(lines).each_with_object({}) do |line, acc| acc[line.vat_rate] = (acc[line.vat_rate] || BigDecimal(0)) + line.net end end |
.per_line(lines) ⇒ Object
Round each line, then sum. Matches an ERP that prices line by line.
63 64 65 66 67 |
# File 'lib/ksef/fa3/summaries.rb', line 63 def per_line(lines) summable(lines).each_with_object({}) do |line, acc| acc[line.vat_rate] = (acc[line.vat_rate] || BigDecimal(0)) + line.vat end end |
.per_summary(nets) ⇒ Object
Sum the nets, then round once. Fewer rounding events, so it can differ from :per_line by a grosz — which is exactly why the choice is explicit.
71 72 73 74 75 76 77 |
# File 'lib/ksef/fa3/summaries.rb', line 71 def per_summary(nets) nets.to_h do |code, net| percentage = VatRate.percentage(code) rounded = percentage ? (net * percentage / 100).round(Formatting::AMOUNT_SCALE) : BigDecimal(0) [code, rounded] end end |
.summable(lines) ⇒ Object
StanPrzed rows are excluded, and that is a correctness fix rather than a tidy.
A row marked "stan przed korektą" states the position as it was; a correction shows
it beside its replacement. Adding the two together answers a question nobody asked:
on the Ministry's Przykład 2 it gave 3089.42 — 1626.01 before plus 1463.41 after —
against a net_total of −162.60. A caller building a per-rate VAT report over
downloaded invoices got a figure nineteen times the truth, with no error and a
passing #valid?.
Nothing that derives a summary is affected: tier 1's SummaryChecks already
refuses a state_before row on an invoice that derives, so these rows only ever
appear where the summary is stated.
A non-Line entry is skipped rather than raised on, matching what
ModelValidator#line_errors and SummaryChecks already tolerate — this runs inside
Invoice.new, and blowing up here would turn a reportable tier-1 issue into a
NoMethodError outside this gem's hierarchy.
46 47 48 |
# File 'lib/ksef/fa3/summaries.rb', line 46 def summable(lines) lines.select { |line| line.is_a?(Line) && !line.state_before && line.summarised? } end |
.vat_by_rate(lines, rounding) ⇒ Object
50 51 52 53 54 |
# File 'lib/ksef/fa3/summaries.rb', line 50 def vat_by_rate(lines, rounding) return per_line(lines) if rounding == :per_line per_summary(net_by_rate(lines)) end |
Instance Method Details
#gross_total ⇒ Object
A stated P_15 wins over a derived one — it is what the document says is owed, and
bucket-level rounding means the derivation can be a grosz out
(Invoice.scaled_gross).
104 |
# File 'lib/ksef/fa3/summaries.rb', line 104 def gross_total = totals&.gross || stated_gross || (net_total + vat_total) |
#net_by_rate ⇒ Hash{String => BigDecimal}
This is what the lines say, always — Invoice#totals does not enter into it,
because a stated summary is keyed by bucket and cannot be resolved back to rate codes
(§8.1a). For a correction that states its own summary the two are different questions,
and an invoice with no lines answers {} here while #net_total answers what the
document declares.
A row with no amount, or none with a rate to bucket it under, contributes nothing. It is legal — see Line#net — and tier 1 is what stops one appearing on an invoice that derives its summary from its rows.
91 |
# File 'lib/ksef/fa3/summaries.rb', line 91 def net_by_rate = Summaries.net_by_rate(lines) |
#net_total ⇒ Object
The three figures the document actually carries: read from Invoice#totals when the invoice states them, computed from the lines otherwise.
98 |
# File 'lib/ksef/fa3/summaries.rb', line 98 def net_total = totals ? totals.net : net_by_rate.values.sum(BigDecimal(0)) |
#vat_by_rate ⇒ Hash{String => BigDecimal}
94 |
# File 'lib/ksef/fa3/summaries.rb', line 94 def vat_by_rate = Summaries.vat_by_rate(lines, rounding) |
#vat_total ⇒ Object
99 |
# File 'lib/ksef/fa3/summaries.rb', line 99 def vat_total = totals ? totals.vat : vat_by_rate.values.sum(BigDecimal(0)) |