InvoiceCalculator
in package
Recalculates the monetary fields of an invoice from its primary inputs (price, quantity, VAT rate, discount).
Called from InvoiceManager::beforeSave() before the flush.
Client or form totals are never stored; they are overwritten by the recomputed values.
invoice-calc.js mirrors this math for UX only; the server is the authority. The calculator
works purely in memory and does no database queries.
Per line (items, shippers and payment methods share the same field set):
vatPercent=useVat?vat: 0;discountPercent=useDiscount?discount: 0.discountInMoney=price*discountPercent/ 100 (per unit).priceAfterDiscount=price-discountInMoney;priceWithVat=price* (1 +vatPercent/ 100).totalPrice=priceAfterDiscount*quantity;totalPriceWithVatusespriceAfterDiscountWithVat.vatInMoney=totalPriceWithVat-totalPrice.
Header: base is the sum of line totals. A voucher discounts base by its percents, plus a fixed
price whose priceWithVat is derived from voucher.vat (never trusted from the client). The voucher
VAT applies only when the header useVat is on; the header wins over line settings.
No whole-currency rounding is applied: roundedTotal* equal total* and rounding* are 0.
IMPORTANT: all arithmetic is bcmath, never floats. Binary floats cannot represent exact halves
(1.750 * 1.21 becomes 2.11749999...), so round() rounds some amounts down by 0.001 and the
invoice totals would differ by a cent from the correct half-up result.
- Inputs are read as numeric strings straight from the decimal getters;
num()only guards against non-numeric or float input. - Intermediate values use
WORK_SCALE= 12 decimals; money output goes throughmoney()(half-up atMONEY_SCALE= 3, half away from zero for negatives such as credit notes). recalculate()sums the unrounded line results and rounds only the header and stored values.
IMPORTANT: scale 12, not 6 like OrderCalculator::WORK_SCALE. The e-shop rounds each line before
summing; the invoicer deliberately does not, and its intermediates reach 11 decimals
(discounted price 7, times the VAT multiplier 9, times a decimal quantity 11). At scale 6 those digits
are truncated and the result is no longer a correct half-up rounding. Scale 12 holds every intermediate exactly.
The same applies to the voucher VAT, a float with 6 decimals.
The front end mirrors this in PriceTrait: the charged payment amount must match the per-line invoice breakdown to the cent.
Table of Contents
Constants
- MONEY_SCALE : int = 3
- Money output scale, matching the `decimal(10,3)` columns.
- WORK_SCALE : int = 12
- Intermediate scale for unrounded line subtotals and rates; see the class docblock for why 12.
Methods
- recalculate() : void
- chargeableLines() : iterable<string|int, InvoiceItem|InvoiceShipper|InvoicePaymentMethod|InvoiceTemplateItem|InvoiceTemplateShipper|InvoiceTemplatePaymentMethod>
- Returns the chargeable lines (items, shippers, payment methods).
- money() : numeric-string
- Formats a money value for the `decimal(10,3)` columns, without thousands separators.
- num() : numeric-string
- Converts an entity getter value to a numeric string safe for bcmath.
- recalculateLine() : array{0: numeric-string, 1: numeric-string}
- Recalculates the monetary fields of one line and returns its [totalPrice, totalPriceWithVat].
- vatMultiplier() : numeric-string
- Returns the VAT multiplier `1 + rate / 100` at `WORK_SCALE`.
- voucherDiscounts() : array{0: numeric-string, 1: numeric-string}
- Sums the voucher discounts and returns [discount, discountWithVat].
Constants
MONEY_SCALE
Money output scale, matching the `decimal(10,3)` columns.
private
int
MONEY_SCALE
= 3
WORK_SCALE
Intermediate scale for unrounded line subtotals and rates; see the class docblock for why 12.
private
int
WORK_SCALE
= 12
Methods
recalculate()
public
recalculate(Invoice|InvoiceTemplate $document) : void
Parameters
- $document : Invoice|InvoiceTemplate
chargeableLines()
Returns the chargeable lines (items, shippers, payment methods).
private
chargeableLines(Invoice|InvoiceTemplate $document) : iterable<string|int, InvoiceItem|InvoiceShipper|InvoicePaymentMethod|InvoiceTemplateItem|InvoiceTemplateShipper|InvoiceTemplatePaymentMethod>
Vouchers are discounts (see voucherDiscounts()) and gifts are non-monetary, so neither is included.
Parameters
- $document : Invoice|InvoiceTemplate
Return values
iterable<string|int, InvoiceItem|InvoiceShipper|InvoicePaymentMethod|InvoiceTemplateItem|InvoiceTemplateShipper|InvoiceTemplatePaymentMethod>money()
Formats a money value for the `decimal(10,3)` columns, without thousands separators.
private
money(numeric-string $value) : numeric-string
Rounds half away from zero: bcmath truncates, so the method adds 0.0005 (-0.0005 for negatives). See PriceDisplayHelper::round3().
Parameters
- $value : numeric-string
Return values
numeric-stringnum()
Converts an entity getter value to a numeric string safe for bcmath.
private
num(int|float|string $value) : numeric-string
Money and percent columns are decimal and their getters return numeric strings, which pass
through unchanged. Only edge inputs are handled: an int shipper/payment quantity, the float
invoice_vouchers.vat and a non-numeric or exponent string (bcmath throws ValueError on it).
Parameters
- $value : int|float|string
Return values
numeric-stringrecalculateLine()
Recalculates the monetary fields of one line and returns its [totalPrice, totalPriceWithVat].
private
recalculateLine(InvoiceItem|InvoiceShipper|InvoicePaymentMethod|InvoiceTemplateItem|InvoiceTemplateShipper|InvoiceTemplatePaymentMethod $line, bool $useVat) : array{0: numeric-string, 1: numeric-string}
The returned values are unrounded intermediates at WORK_SCALE; the header sums them and
rounds only the total.
Parameters
- $line : InvoiceItem|InvoiceShipper|InvoicePaymentMethod|InvoiceTemplateItem|InvoiceTemplateShipper|InvoiceTemplatePaymentMethod
- $useVat : bool
Return values
array{0: numeric-string, 1: numeric-string}vatMultiplier()
Returns the VAT multiplier `1 + rate / 100` at `WORK_SCALE`.
private
vatMultiplier(numeric-string $percents) : numeric-string
Parameters
- $percents : numeric-string
Return values
numeric-stringvoucherDiscounts()
Sums the voucher discounts and returns [discount, discountWithVat].
private
voucherDiscounts(Invoice|InvoiceTemplate $document, numeric-string $base, numeric-string $baseVat, int|null $currencyId, bool $useVat) : array{0: numeric-string, 1: numeric-string}
A voucher may carry percents and a fixed amount at once: the percentage is taken from the base
(without and with VAT) first, then the fixed amount is added. Sets the voucher currency from the
header. The voucher vat applies only to VAT payers, as for lines (see recalculate()).
Parameters
- $document : Invoice|InvoiceTemplate
- $base : numeric-string
- $baseVat : numeric-string
- $currencyId : int|null
- $useVat : bool