OrderInvoiceGenerator
in package
Issues the Invoicer document (regular invoice or credit note) for an order and pairs the payment (`markPaid`).
The input is the order snapshot (order.*/orderItem.* columns), never the live catalog.
It runs under the per-order lock held by OrderTransitionManager and does not take the lock itself.
Idempotence: for a type with multipleUse=false, existingInvoice() returns the existing
document unchanged. Credit note types have multipleUse=true, so a new one is always issued
(a mirror of the last non-credit invoice of the order, see buildCreditNote()).
Settings priority (billingLine/seller/productType/userType/paymentRegion): 4/invoicer.<key>
(an empty string counts as missing) falls back to 7/invoice.default.<key>. Callers pass the
whole module 7 key; the module 4 key is derived by eshopInvoicerKey(). paymentRegionId also
has an order axis (order.paymentRegion) that wins over both settings layers; billingLineId
is won by the invoice type (InvoiceType::getBillingLineId()) before the settings chain.
The seller likewise comes from the whole order snapshot (order.seller*/order.bank*, see
applySellerFromOrder()); the settings chain is only a fallback for older orders.
IMPORTANT: Invoicer does not compute rounding (rounding* is a no-op), so order.roundingVat > 0
gets an extra rounding line with vat 0 (see appendRoundingLine()). Without it the document would
not match the amount paid.
Vouchers always go to the document as a fixed amount, even percentage ones (see appendVouchers()).
Parity check after save: |invoice.totalPriceWithVat - order.roundedTotalPriceWithVat| within
0.5 %. A mismatch does not cancel the document; it logs a Tracy warning (channel eshop.general) and
adds the Result warning parityMismatch (parityOk=false).
Tags
Table of Contents
Constants
- PERMANENT_ERROR_CODES : array<string|int, mixed> = [ 'sourceInvoiceMissing', 'negativeTotal', 'not...
- `errorCode` values for which a retry is pointless; the invoice catch-up cron ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderCronTrait::runInvoiceCatchup()}) moves its watermark past them so the order does not eat the batch forever.
- CREDIT_NOTE_TYPE_KEY : string = 'DOBROPIS'
- Invoice type key for a partial credit note. There are two credit types (cancellation and credit note both have `credit_note=1`), but the partial mode applies only to the credit note: a cancellation voids an unpaid invoice and stays whole-order.
- DEFAULT_DUE_PERIOD : int = 14
- ESHOP_INVOICER_PREFIX : string = 'invoicer.'
- ESHOP_SETTING_MODULE : int = 4
- INVOICER_DEFAULT_PREFIX : string = 'invoice.default.'
- Suffix shared by the keys of both layers: module 7 has it after `invoice.default.`, module 4 after `invoicer.`.
- INVOICER_SETTING_MODULE : int = 7
- MONEY_SCALE : int = 3
- ORDER_DOCUMENT_TYPE_KEY : string = 'ORDER'
- Key of the non-tax invoice type "order confirmation" (`cms_mod_invoicer_invoice_types.key`).
- PARITY_TOLERANCE_RATIO : float = 0.005
- TEXT_CREDIT_NOTE : string = '//document.invoicer.creditNote.note'
- TEXT_ROUNDING : string = '//document.invoicer.lines.rounding'
- Keys of the texts the generator writes into the document (app/Locale/invoicer/<locale>/document.*.neon).
- VOUCHER_CAP_TOLERANCE : string = '0.010'
- How far a voucher row may exceed the calculator's capped discount before it is clamped.
- WORK_SCALE : int = 6
Properties
- $calculator : OrderCalculator
- $creditNoteLedger : CreditNoteLedger
- $invoiceManager : InvoiceManager
- $invoiceTypeManager : InvoiceTypeManager
- $localizationManager : LocalizationManager
- $orderManagerFactory : OrderManagerFactory
- $paymentRegionManager : PaymentRegionManager
- $sellerManager : SellerManager
- $settingContext : SettingContext
- $taxManager : TaxManager
- $translator : Translator
Methods
- __construct() : mixed
- findCreditNoteSource() : Invoice|null
- Returns the source invoice for a credit note: the last non-credit document of the order, with all charged collections.
- generateForStatus() : Result
- Issues the document for the target status (`status.createInvoice=1`).
- generatePartialCreditNote() : Result
- Issues a partial credit note for selected lines of the source invoice.
- markPaid() : Result
- Sets all existing non-credit invoices of the order to status `paid` (`updateColumns`).
- regenerateOrderDocument() : Result
- Regenerates the ORDER document on every edit of the order.
- ancillaryLineName() : string
- Name of an ancillary supply line, with the rate in brackets only when split over several rates.
- appendCreditNoteVouchers() : void
- Appends the vouchers to the credit note: the discount is split proportionally and automatically (the admin does not see it in the form), because a corrective tax document must carry the proportional difference of base and tax; a discount on the whole order across VAT rates is split between them proportionally.
- appendLines() : void
- Appends the lines: item to `InvoiceItem` (see {@see combinedDiscountPercent()}), shipper and payment method 1:1, vouchers, gifts, and the rounding line ({@see appendRoundingLine()}).
- appendRoundingLine() : void
- Appends the rounding line.
- appendVouchers() : void
- Appends the vouchers.
- applyCustomer() : void
- Applies the customer, denormalized from `order.customer*` (and `customer_dlv_*` for a delivery address).
- applySeller() : void
- Applies the seller from the directory (like `InvoiceGenerator::applySeller()`).
- applySellerFromOrder() : bool
- Applies the seller from the order snapshot (the admin fixed it there by the edit form).
- applyTaxSnapshot() : void
- Copies the VAT snapshot of the order to the document.
- buildCreditNote() : Invoice
- Builds the credit note: a mirror of the header of the last non-credit invoice of the ORDER (not of the payment, unlike `InvoiceGenerator::buildCreditNote()` in the payment flow) with negated lines.
- buildCreditNoteWithinLimit() : Result
- Builds the credit note and enforces the limit.
- buildFromOrder() : Invoice|null
- chargeableBase() : numeric-string
- Returns the base of the charged lines (items, shipping, payment; not vouchers and gifts, exactly like `InvoiceCalculator::chargeableLines()`). `$lines = null` means the whole invoice, otherwise only the selected lines in the selected quantity.
- checkParity() : bool
- Checks parity after save: `|invoice.totalPriceWithVat - order.roundedTotalPriceWithVat|` within 0.5 %.
- checkParityUnlessCreditNote() : bool
- Checks parity, except for a credit note.
- combinedDiscountPercent() : float
- Computes the combined discount percent of a line.
- creditNoteTypeId() : int|null
- Returns the id of the invoice type `key='DOBROPIS'` (like `orderDocumentTypeId()`), or `null` when the seed is missing.
- documentLocale() : string
- documentText() : string
- Returns a text written to the document in the order language.
- documentUnit() : string
- effectiveVatPercent() : float
- Computes the effective VAT rate of a discount in % from (base, with VAT); `InvoiceCalculator` derives `priceWithVat` back from it. A zero base gives 0.
- eshopInvoicerKey() : string|null
- Returns the Eshop (module 4) override key for the Invoicer (module 7) default key: `invoice.default.sellerId` becomes `invoicer.sellerId`. A key outside `invoice.default.*` has no override.
- exhaustsSource() : bool
- Tells whether this credit note zeroes the remainder of the whole invoice; then it settles the cent remainders of vouchers.
- existingInvoice() : Invoice|null
- Idempotence: checks whether a document of the given type linked to this ORDER already exists.
- feeDescription() : string|null
- Builds the informative item description "Recycling fee: 10.00" (per piece, several fees separated by a semicolon); `null` when the item has no fee.
- findOrderDocument() : Invoice|null
- Returns the existing ORDER document of the order with all collections that {@see appendLines()} overwrites (the `includes` must cover everything `save()` has to keep).
- findSourceInvoiceForOrder() : Invoice|null
- Returns the last (newest) non-credit invoice of the ORDER, the credit note source. Includes all charged collections and vouchers (mirror).
- hasLegacyFeesInPrice() : bool
- Tells whether the snapshot is historical: it added fees to `item.price` (`price != priceWithoutFees`).
- localizationRef() : Localization|null
- Returns a thin localization reference for writing to the document; `InvoiceManager::save()` resolves it by identity through `hydrateForPersist()` (the generator must not hold the EntityManager).
- mirrorLine() : void
- Mirrors a credit note line: copies name, quantity, unit, vat, useVat and discounts, with the price x -1. The derived fields (`priceAfterDiscount`, `total*`) are recomputed by `InvoiceCalculator`; being linear, it gives exactly the negative of the originally charged line.
- money() : numeric-string
- Normalizes a money string for bcmath (a non-numeric or empty value gives zero; like {@see OrderCalculator::norm()}).
- negatePrice() : string
- nonCreditInvoiceTypeIds() : array<int, int>
- Returns ids of document types that are not a credit note (`credit_note=false`): candidates for the credit note source and the target of `markPaid()`.
- nullableMoney() : string|null
- A zero fixed voucher amount means no discount (`InvoiceCalculator` would only add 0).
- orderDocumentTypeId() : int|null
- Returns the id of the non-tax document type `key='ORDER'`, or `null` when the seed is missing.
- orderLocalization() : object|null
- Returns the order localization for the number format in the document texts.
- percentOf() : numeric-string
- Computes `round3(base x percents/100)`, bit for bit the formula of {@see OrderCalculator::recalculate()}.
- percentShare() : numeric-string
- Computes the share of a row in the total percentage discount, proportional to its `percents` (the last row settles the remainder).
- remainingLines() : array<string, array<int, int>>
- Returns "all that remains", the input of a whole-order credit note. Lines with a zero remainder drop out.
- resolveBillingLineId() : int|null
- Returns the line for a document: the own line of the type, else the settings chain; otherwise null.
- round3() : numeric-string
- Rounds half up to 3 decimal places (bcmath truncates; like {@see OrderCalculator::round3()}).
- settingInt() : int
- settingIntOrNull() : int|null
- taxIdForInvoice() : string|null
- Copies the company id and tax id from `orders.customer_*` (`varchar(255)`) to `invoices.customer_*` (`varchar(20)`).
- totalRequestedQuantity() : int
- translateTaxNote() : string|null
- Returns the VAT clause in the document language (the order language) and Czech; `null` when the code has no clause.
- watermarkInvoicedStatus() : Result|null
- Returns `null` on success, otherwise a transient failure (see below).
Constants
PERMANENT_ERROR_CODES
`errorCode` values for which a retry is pointless; the invoice catch-up cron ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderCronTrait::runInvoiceCatchup()}) moves its watermark past them so the order does not eat the batch forever.
public
array<string|int, mixed>
PERMANENT_ERROR_CODES
= [
'sourceInvoiceMissing',
'negativeTotal',
'nothingToCredit',
'creditLimitExceeded',
'unattributedCreditLines',
'creditNoteTypeMissing',
// The credit limit handles whole pieces only. Decimal quantities do occur (billing from a work
// sheet produces them), so this state is reachable. A retry does not help: the document does
// not change, the credit note is resolved manually in Invoicer.
'decimalQuantityUnsupported',
]
IMPORTANT: every new permanent reason must be added here, not to a copy of the list at the
caller. nothingToCredit is routine (an order in status 9 whose credit note already exists);
treated as transient, the cron would retry it on every run forever.
CREDIT_NOTE_TYPE_KEY
Invoice type key for a partial credit note. There are two credit types (cancellation and credit note both have `credit_note=1`), but the partial mode applies only to the credit note: a cancellation voids an unpaid invoice and stays whole-order.
private
string
CREDIT_NOTE_TYPE_KEY
= 'DOBROPIS'
DEFAULT_DUE_PERIOD
private
int
DEFAULT_DUE_PERIOD
= 14
ESHOP_INVOICER_PREFIX
private
string
ESHOP_INVOICER_PREFIX
= 'invoicer.'
ESHOP_SETTING_MODULE
private
int
ESHOP_SETTING_MODULE
= 4
INVOICER_DEFAULT_PREFIX
Suffix shared by the keys of both layers: module 7 has it after `invoice.default.`, module 4 after `invoicer.`.
private
string
INVOICER_DEFAULT_PREFIX
= 'invoice.default.'
INVOICER_SETTING_MODULE
private
int
INVOICER_SETTING_MODULE
= 7
MONEY_SCALE
private
int
MONEY_SCALE
= 3
ORDER_DOCUMENT_TYPE_KEY
Key of the non-tax invoice type "order confirmation" (`cms_mod_invoicer_invoice_types.key`).
private
string
ORDER_DOCUMENT_TYPE_KEY
= 'ORDER'
PARITY_TOLERANCE_RATIO
private
float
PARITY_TOLERANCE_RATIO
= 0.005
TEXT_CREDIT_NOTE
private
string
TEXT_CREDIT_NOTE
= '//document.invoicer.creditNote.note'
TEXT_ROUNDING
Keys of the texts the generator writes into the document (app/Locale/invoicer/<locale>/document.*.neon).
private
string
TEXT_ROUNDING
= '//document.invoicer.lines.rounding'
VOUCHER_CAP_TOLERANCE
How far a voucher row may exceed the calculator's capped discount before it is clamped.
private
string
VOUCHER_CAP_TOLERANCE
= '0.010'
WORK_SCALE
private
int
WORK_SCALE
= 6
Properties
$calculator read-only
private
OrderCalculator
$calculator
$creditNoteLedger read-only
private
CreditNoteLedger
$creditNoteLedger
$invoiceManager read-only
private
InvoiceManager
$invoiceManager
$invoiceTypeManager read-only
private
InvoiceTypeManager
$invoiceTypeManager
$localizationManager read-only
private
LocalizationManager
$localizationManager
$orderManagerFactory read-only
private
OrderManagerFactory
$orderManagerFactory
$paymentRegionManager read-only
private
PaymentRegionManager
$paymentRegionManager
$sellerManager read-only
private
SellerManager
$sellerManager
$settingContext read-only
private
SettingContext
$settingContext
$taxManager read-only
private
TaxManager
$taxManager
$translator read-only
private
Translator
$translator
Methods
__construct()
public
__construct(SettingContext $settingContext, InvoiceManager $invoiceManager, InvoiceTypeManager $invoiceTypeManager, SellerManager $sellerManager, PaymentRegionManager $paymentRegionManager, OrderCalculator $calculator, CreditNoteLedger $creditNoteLedger, OrderManagerFactory $orderManagerFactory, TaxManager $taxManager, Translator $translator, LocalizationManager $localizationManager) : mixed
Parameters
- $settingContext : SettingContext
- $invoiceManager : InvoiceManager
- $invoiceTypeManager : InvoiceTypeManager
- $sellerManager : SellerManager
- $paymentRegionManager : PaymentRegionManager
- $calculator : OrderCalculator
- $creditNoteLedger : CreditNoteLedger
- $orderManagerFactory : OrderManagerFactory
- $taxManager : TaxManager
- $translator : Translator
- $localizationManager : LocalizationManager
findCreditNoteSource()
Returns the source invoice for a credit note: the last non-credit document of the order, with all charged collections.
public
findCreditNoteSource(int $orderId) : Invoice|null
Public for the credit note preview (OrderCreditNoteManager::preview()): if the caller looked it up itself, it could diverge from the invoice the credit note is really built from, and the UI would offer lines of another document.
Parameters
- $orderId : int
Return values
Invoice|nullgenerateForStatus()
Issues the document for the target status (`status.createInvoice=1`).
public
generateForStatus(Order $order, OrderStatus $status) : Result
Success data: ['invoiceId'=>int,'existing'=>bool,'pdf'=>?string,'parityOk'=>bool, 'invoiceNumber'=>?string,'warnings'=>list<string>]. Failure only when the document cannot be
issued (missing type, numbering line, seller or credit source); the caller treats it as fail-soft.
Failure errorCode in data: sourceInvoiceMissing (the type has requires_invoice and the
order has no non-credit invoice) and negativeTotal; for a credit note type also every code of
buildCreditNoteWithinLimit(). All of them are in PERMANENT_ERROR_CODES: the caller
must not report them as "cron will catch up" and the cron must not retry them forever. Other
failures are transient (fail-soft, the cron catches up).
Parameters
- $order : Order
- $status : OrderStatus
Return values
ResultgeneratePartialCreditNote()
Issues a partial credit note for selected lines of the source invoice.
public
generatePartialCreditNote(TOrderGraph $order, array<string, array<int, int>> $requestedLines) : Result
This is not a status transition: returning 1 of 3 pieces does not make the order "returned",
so the order status and the invoiced_status watermark stay untouched. The admin switches to
status 9 manually.
IMPORTANT: must run under the per-order lock held by OrderCreditNoteManager; this method does not take it (same contract as generateForStatus()). Without the lock two concurrent requests would read the same balance and both pass the limit.
Failure errorCode: creditNoteTypeMissing plus everything buildCreditNoteWithinLimit()
returns, including decimalQuantityUnsupported (the source invoice has a line with a decimal
quantity; the credit limit handles whole pieces only).
Parameters
- $order : TOrderGraph
- $requestedLines : array<string, array<int, int>>
-
collection => (source line id => pieces); collection keys are the constants of CreditNoteLedger
Return values
ResultmarkPaid()
Sets all existing non-credit invoices of the order to status `paid` (`updateColumns`).
public
markPaid(Order $order) : Result
Idempotent: a second call finds nothing to update (updated=0).
Parameters
- $order : Order
Return values
ResultregenerateOrderDocument()
Regenerates the ORDER document on every edit of the order.
public
regenerateOrderDocument(TOrderGraph $order) : Result
OrderManager::completedSave() calls this after every
update (the insert goes through generateForStatus(), the same path as the status transition
step). Unlike generateForStatus() it deliberately bypasses the idempotence
existingInvoice() with !multipleUse. That idempotence protects numbered tax documents
(proforma, invoice, cancellation, credit note) from regeneration after issue, because their
content must not change retroactively. The ORDER type is non-tax (requires_invoice=0,
credit_note=0): only a downloadable copy of the order, not an accounting record, so it may
be overwritten in the same row (same id, number and billing line; new content and PDF).
Tax documents never go through this method (filter on key='ORDER' in
orderDocumentTypeId()).
When the order has an existing ORDER document, it overwrites the content (seller, customer,
items, shipping, payment, vouchers, gifts; like buildFromOrder() but on the existing
entity, with a full replace of the collections through appendLines()) and saves it by
this save() (an update); InvoiceManager::completedSave() regenerates the PDF/QR
automatically. When there is none (an order created before this feature, or the ORDER type
has no configuration or create_invoice), it creates nothing: this only refreshes an
existing document, creating is done by generateForStatus().
Parameters
- $order : TOrderGraph
Return values
ResultancillaryLineName()
Name of an ancillary supply line, with the rate in brackets only when split over several rates.
private
ancillaryLineName(string $name, string $vat, int $partCount) : string
Parameters
- $name : string
- $vat : string
- $partCount : int
Return values
stringappendCreditNoteVouchers()
Appends the vouchers to the credit note: the discount is split proportionally and automatically (the admin does not see it in the form), because a corrective tax document must carry the proportional difference of base and tax; a discount on the whole order across VAT rates is split between them proportionally.
private
appendCreditNoteVouchers(Invoice $invoice, Invoice $source, array<string, array<int, int>> $lines, TCreditNoteLedger $ledger, bool $isFinal) : void
Two branches, both needed:
percents(only invoices created manually in Invoicer): copy 1:1. The calculator computesbase x percents/100from the base of this document, so proportionality comes out by itself.- fixed amount (always on the e-shop path, appendVouchers() writes
percents = null): split by the share of the returned base. The last credit note (the one that zeroes the remainder) settles the cent remainder from the ledger, so the credit notes sum to the invoice exactly, the same pattern asappendVouchers()uses for$lastPercentIndex.
Parameters
appendLines()
Appends the lines: item to `InvoiceItem` (see {@see combinedDiscountPercent()}), shipper and payment method 1:1, vouchers, gifts, and the rounding line ({@see appendRoundingLine()}).
private
appendLines(Invoice $invoice, Order $order) : void
Fees (recycling, copyright...) are informative: the item price already contains them, so they
get no charged line of their own and appear only in the item description
(feeDescription()). The exception is a historical order whose snapshot added the fee to
item.price (price != priceWithoutFees): there the document still splits the item into the
base and fee lines with the same discount (percentages are linear, so the sum is the same);
otherwise the document would not match the amount already paid.
Parameters
appendRoundingLine()
Appends the rounding line.
private
appendRoundingLine(Invoice $invoice, Order $order) : void
IMPORTANT: Invoicer does not compute rounding (rounding* is a no-op), so order.roundingVat > 0
gets an extra line with vat=0; without it the document would not match the amount paid.
The base side (totalPrice without VAT) shifts the same way as the with-VAT side (vat 0 means
price === priceWithVat for this line); parity is measured on the with-VAT side.
Parameters
appendVouchers()
Appends the vouchers.
private
appendVouchers(Invoice $invoice, TOrderGraph $order) : void
IMPORTANT: a voucher always goes to the document as a fixed amount (percents = null), even
when it is a percentage one on the order. The two calculations take the percentage from
different bases:
- OrderCalculator:
round3(itemsTotal x p/100),p = min(100, sum of percents), from the items only (no shipping, payment or rounding line) and capped at 100 %; - InvoiceCalculator:
base x percents/100from all charged lines of the document (items, fee lines, shipping, payment, rounding line), with no cap, summed over all vouchers. A percentage row would therefore undercharge the document against the amount the customer really pays (order.roundedTotalPriceWithVat).
The split into rows keeps voucher codes visible: each row carries its fixed part
(OrderVoucher::price/priceWithVat, completed by OrderCalculator with the max item rate)
plus a share of the percentage discount proportional to its percents. The last percentage
row settles the remainder, so the rows sum to the discount computed by OrderCalculator.
The row vat is the effective rate derived from (base, with VAT): InvoiceCalculator derives
priceWithVat itself as price x (1 + vat/100) and does not trust a posted value, so the
rate is the only way to pass both sides of the discount. The column invoice_vouchers.vat is
float (unlike invoice_items.vat = decimal(5,2)), so the effective rate keeps its full precision.
Parameters
- $invoice : Invoice
- $order : TOrderGraph
applyCustomer()
Applies the customer, denormalized from `order.customer*` (and `customer_dlv_*` for a delivery address).
private
applyCustomer(Invoice $invoice, Order $order) : void
order.customerId/customerContactId (find-or-create by e-mail, see
OrderCustomerManager) are copied to the invoice. The
name and address fields still come from the order, not from the customer record: the document
reflects the state at the time of the order. For a company order name2 carries the person's
"first name surname" when the company is filled.
Parameters
applySeller()
Applies the seller from the directory (like `InvoiceGenerator::applySeller()`).
private
applySeller(Invoice $invoice, int $sellerId) : void
Parameters
- $invoice : Invoice
- $sellerId : int
applySellerFromOrder()
Applies the seller from the order snapshot (the admin fixed it there by the edit form).
private
applySellerFromOrder(Invoice $invoice, Order $order) : bool
There is no directory query: the columns order.seller*/order.bank* mirror the invoice by
name and are copied 1:1, even when the seller changed the data meanwhile or someone deleted it
from the directory (sellerId is a reference without a foreign key).
Parameters
Return values
bool —false when the order has no snapshot (created before it existed); the caller
then falls back to applySeller() on the live directory
applyTaxSnapshot()
Copies the VAT snapshot of the order to the document.
private
applyTaxSnapshot(Invoice $invoice, TOrderGraph $order) : void
Treatment code, rate country, tax id verification, summary report code and flags are copied
1:1; the clause is translated again in the document language (and Czech). tax_date is the
tax point of the document, i.e. its issue date (for the e-shop the payment or dispatch day).
An order taxed at another date is recognized by the flag RATE_DATE_MISMATCH, which is added
here and resolved by the admin.
An order without a snapshot (before the VAT engine) leaves the columns null; the document then goes the old PDF matrix way.
Parameters
- $invoice : Invoice
- $order : TOrderGraph
buildCreditNote()
Builds the credit note: a mirror of the header of the last non-credit invoice of the ORDER (not of the payment, unlike `InvoiceGenerator::buildCreditNote()` in the payment flow) with negated lines.
private
buildCreditNote(Order $order, Invoice $source, int $creditTypeId, int $billingLineId, array<string, array<int, int>> $lines, TCreditNoteLedger $ledger, bool $isFinal) : Invoice
The header is copied like in mirrorLine(); id, number, sequence order, pdf and qr are
never carried over, while relatedInvoiceId, the line and the dates are set anew.
Only the selected lines in the selected quantity are mirrored (buildCreditNoteWithinLimit() already checked that they fit into the remainder).
Parameters
- $order : Order
- $source : Invoice
- $creditTypeId : int
- $billingLineId : int
- $lines : array<string, array<int, int>>
-
collection => (source line id => pieces)
- $ledger : TCreditNoteLedger
- $isFinal : bool
-
the credit note zeroes the remainder of the whole invoice, so it settles the cent remainders of vouchers
Return values
InvoicebuildCreditNoteWithinLimit()
Builds the credit note and enforces the limit.
private
buildCreditNoteWithinLimit(TOrderGraph $order, int $creditTypeId, int $billingLineId, array<string, array<int, int>>|null $requested) : Result
This is the only path to a credit note: both the whole-order one ($requested = null means
"all that remains") and the partial one must pass here, otherwise one path would bypass the
limit of the other.
Failure errorCode: sourceInvoiceMissing, unattributedCreditLines, nothingToCredit,
creditLimitExceeded, decimalQuantityUnsupported. All are permanent: a retry does not help
and the cron must not retry them forever (same class as negativeTotal).
Parameters
- $order : TOrderGraph
- $creditTypeId : int
- $billingLineId : int
- $requested : array<string, array<int, int>>|null
-
collection => (source line id => pieces)
Return values
Result —data: ['invoice' => Invoice, 'ledger' => array, 'final' => bool]
buildFromOrder()
private
buildFromOrder(Order $order, int $invoiceTypeId, int $billingLineId) : Invoice|null
Parameters
- $order : Order
- $invoiceTypeId : int
- $billingLineId : int
Return values
Invoice|nullchargeableBase()
Returns the base of the charged lines (items, shipping, payment; not vouchers and gifts, exactly like `InvoiceCalculator::chargeableLines()`). `$lines = null` means the whole invoice, otherwise only the selected lines in the selected quantity.
private
chargeableBase(Invoice $source, array<string, array<int, int>>|null $lines) : numeric-string
It scales the stored totalPrice by the quantity ratio: the calculator computes
totalPrice = priceAfterDiscount x quantity, which is linear in the quantity, so the ratio
is right for discounted lines too.
Parameters
- $source : Invoice
- $lines : array<string, array<int, int>>|null
Return values
numeric-stringcheckParity()
Checks parity after save: `|invoice.totalPriceWithVat - order.roundedTotalPriceWithVat|` within 0.5 %.
private
checkParity(Order $order, Invoice $invoice, array<int, string> &$warnings) : bool
A mismatch logs a Tracy warning (channel eshop.general) and adds $warnings[] = 'parityMismatch';
the document is not canceled.
Parameters
Return values
boolcheckParityUnlessCreditNote()
Checks parity, except for a credit note.
private
checkParityUnlessCreditNote(TOrderGraph $order, Invoice $invoice, InvoiceType $invoiceType, array<int, string> &$warnings) : bool
IMPORTANT: parity is not interpreted for a credit note. It mirrors the source invoice with
negated prices, so its totalPriceWithVat is the negative counterpart of the order amount and
|-X - X| = 2X exceeds the 0.5 % tolerance for every non-zero order. Without this gate every
credit note would report parityMismatch and write a warning to the Tracy channel eshop.general.
Parameters
- $order : TOrderGraph
- $invoice : Invoice
- $invoiceType : InvoiceType
- $warnings : array<int, string>
Return values
boolcombinedDiscountPercent()
Computes the combined discount percent of a line.
private
combinedDiscountPercent(string $itemDiscountPercents, string $userDiscountPercents) : float
OrderCalculator composes the item discount and the group discount (userDiscountPercents)
multiplicatively on the unit price. Invoicer has only one percent field per line, so the
combined rate is passed: (1 - combined/100) = (1 - item/100) x (1 - user/100). Without a
group discount combined === item.
Parameters
- $itemDiscountPercents : string
- $userDiscountPercents : string
Return values
floatcreditNoteTypeId()
Returns the id of the invoice type `key='DOBROPIS'` (like `orderDocumentTypeId()`), or `null` when the seed is missing.
private
creditNoteTypeId() : int|null
Return values
int|nulldocumentLocale()
private
documentLocale(TOrderGraph $order) : string
Parameters
- $order : TOrderGraph
Return values
stringdocumentText()
Returns a text written to the document in the order language.
private
documentText(string $key, TOrderGraph $order[, array<string, string> $params = [] ]) : string
A missing translation (the translator returns the key) must not end up on the document as a raw key; it falls back to Czech from the default locale.
Parameters
- $key : string
- $order : TOrderGraph
- $params : array<string, string> = []
Return values
stringdocumentUnit()
private
documentUnit(string|null $code, TOrderGraph $order) : string
Parameters
- $code : string|null
- $order : TOrderGraph
Return values
stringeffectiveVatPercent()
Computes the effective VAT rate of a discount in % from (base, with VAT); `InvoiceCalculator` derives `priceWithVat` back from it. A zero base gives 0.
private
effectiveVatPercent(numeric-string $price, numeric-string $priceWithVat) : float
Parameters
- $price : numeric-string
- $priceWithVat : numeric-string
Return values
floateshopInvoicerKey()
Returns the Eshop (module 4) override key for the Invoicer (module 7) default key: `invoice.default.sellerId` becomes `invoicer.sellerId`. A key outside `invoice.default.*` has no override.
private
static eshopInvoicerKey(string $invoicerKey) : string|null
Parameters
- $invoicerKey : string
Return values
string|nullexhaustsSource()
Tells whether this credit note zeroes the remainder of the whole invoice; then it settles the cent remainders of vouchers.
private
exhaustsSource(TCreditNoteLedger $ledger, array<string, array<int, int>> $lines) : bool
Parameters
- $ledger : TCreditNoteLedger
- $lines : array<string, array<int, int>>
Return values
boolexistingInvoice()
Idempotence: checks whether a document of the given type linked to this ORDER already exists.
private
existingInvoice(int $orderId, int $invoiceTypeId) : Invoice|null
Parameters
- $orderId : int
- $invoiceTypeId : int
Return values
Invoice|nullfeeDescription()
Builds the informative item description "Recycling fee: 10.00" (per piece, several fees separated by a semicolon); `null` when the item has no fee.
private
feeDescription(iterable<string|int, object> $fees, TOrderGraph $order) : string|null
The amount has the same form as the price the customer sees (with VAT for an order with VAT, otherwise the base).
Parameters
- $fees : iterable<string|int, object>
- $order : TOrderGraph
Return values
string|nullfindOrderDocument()
Returns the existing ORDER document of the order with all collections that {@see appendLines()} overwrites (the `includes` must cover everything `save()` has to keep).
private
findOrderDocument(int $orderId, int $invoiceTypeId) : Invoice|null
Parameters
- $orderId : int
- $invoiceTypeId : int
Return values
Invoice|nullfindSourceInvoiceForOrder()
Returns the last (newest) non-credit invoice of the ORDER, the credit note source. Includes all charged collections and vouchers (mirror).
private
findSourceInvoiceForOrder(int $orderId) : Invoice|null
Parameters
- $orderId : int
Return values
Invoice|nullhasLegacyFeesInPrice()
Tells whether the snapshot is historical: it added fees to `item.price` (`price != priceWithoutFees`).
private
hasLegacyFeesInPrice(object $item, string $priceWithoutFees) : bool
A new snapshot has both prices equal (fees are informative).
Parameters
- $item : object
- $priceWithoutFees : string
Return values
boollocalizationRef()
Returns a thin localization reference for writing to the document; `InvoiceManager::save()` resolves it by identity through `hydrateForPersist()` (the generator must not hold the EntityManager).
private
localizationRef(int|null $localizationId) : Localization|null
Parameters
- $localizationId : int|null
Return values
Localization|nullmirrorLine()
Mirrors a credit note line: copies name, quantity, unit, vat, useVat and discounts, with the price x -1. The derived fields (`priceAfterDiscount`, `total*`) are recomputed by `InvoiceCalculator`; being linear, it gives exactly the negative of the originally charged line.
private
mirrorLine(InvoiceItem|InvoiceShipper|InvoicePaymentMethod $from, InvoiceItem|InvoiceShipper|InvoicePaymentMethod $to) : void
Parameters
money()
Normalizes a money string for bcmath (a non-numeric or empty value gives zero; like {@see OrderCalculator::norm()}).
private
money(mixed $value) : numeric-string
Parameters
- $value : mixed
Return values
numeric-stringnegatePrice()
private
negatePrice(string $price) : string
Parameters
- $price : string
Return values
stringnonCreditInvoiceTypeIds()
Returns ids of document types that are not a credit note (`credit_note=false`): candidates for the credit note source and the target of `markPaid()`.
private
nonCreditInvoiceTypeIds() : array<int, int>
Return values
array<int, int>nullableMoney()
A zero fixed voucher amount means no discount (`InvoiceCalculator` would only add 0).
private
nullableMoney(string $value) : string|null
Parameters
- $value : string
Return values
string|nullorderDocumentTypeId()
Returns the id of the non-tax document type `key='ORDER'`, or `null` when the seed is missing.
private
orderDocumentTypeId() : int|null
Return values
int|nullorderLocalization()
Returns the order localization for the number format in the document texts.
private
orderLocalization(TOrderGraph $order) : object|null
IMPORTANT: a lazy proxy from the order graph cannot be initialized after UoW::clear() (or
when the same identity was meanwhile loaded as another object): Doctrine throws
EntityIdentityCollisionException. Returning null would give the document texts the
currency format instead of the localization format and log exceptions on every save of the
order. So a managed instance by id is taken first through LocalizationManager; null
is only the last resort.
Parameters
- $order : TOrderGraph
Return values
object|nullpercentOf()
Computes `round3(base x percents/100)`, bit for bit the formula of {@see OrderCalculator::recalculate()}.
private
percentOf(numeric-string $base, int $percents) : numeric-string
Parameters
- $base : numeric-string
- $percents : int
Return values
numeric-stringpercentShare()
Computes the share of a row in the total percentage discount, proportional to its `percents` (the last row settles the remainder).
private
percentShare(numeric-string $discount, int $percents, int $percentSum) : numeric-string
Parameters
- $discount : numeric-string
- $percents : int
- $percentSum : int
Return values
numeric-stringremainingLines()
Returns "all that remains", the input of a whole-order credit note. Lines with a zero remainder drop out.
private
remainingLines(TCreditNoteLedger $ledger) : array<string, array<int, int>>
Parameters
- $ledger : TCreditNoteLedger
Return values
array<string, array<int, int>>resolveBillingLineId()
Returns the line for a document: the own line of the type, else the settings chain; otherwise null.
private
resolveBillingLineId(InvoiceType $invoiceType) : int|null
Parameters
- $invoiceType : InvoiceType
Return values
int|nullround3()
Rounds half up to 3 decimal places (bcmath truncates; like {@see OrderCalculator::round3()}).
private
round3(numeric-string $value) : numeric-string
Parameters
- $value : numeric-string
Return values
numeric-stringsettingInt()
private
settingInt(string $key, int $default) : int
Parameters
- $key : string
-
module 7 key (
invoice.default.*) - $default : int
Return values
intsettingIntOrNull()
private
settingIntOrNull(string $key) : int|null
Parameters
- $key : string
-
module 7 key (
invoice.default.*)
Return values
int|nulltaxIdForInvoice()
Copies the company id and tax id from `orders.customer_*` (`varchar(255)`) to `invoices.customer_*` (`varchar(20)`).
private
taxIdForInvoice(string|null $value, string $property, int|null $orderId) : string|null
IMPORTANT: a truncated tax value on a tax document must never appear silently. The invoice
column is 235 characters narrower, so a longer value would otherwise fail the invoice with a
generic database error (strict sql_mode) or be cut without any record (non-strict). The primary gate is the checkout
(AddressFormTrait); this is the
second defense for older orders that already hold a longer value. The cut is explicit,
measured by the width of the invoice column, and logged.
Issuing does not fail because of it: the order is paid, the document must exist. The difference is found in the log and fixed on the invoice manually.
Parameters
- $value : string|null
- $property : string
- $orderId : int|null
Return values
string|nulltotalRequestedQuantity()
private
totalRequestedQuantity(array<string, array<int, int>> $lines) : int
Parameters
- $lines : array<string, array<int, int>>
Return values
inttranslateTaxNote()
Returns the VAT clause in the document language (the order language) and Czech; `null` when the code has no clause.
private
translateTaxNote(TOrderGraph $order, string|null $noteKey) : string|null
The language comes from the order entity (documentLocale()), not through a manager: an Api read in the middle of building the document would empty the identity map.
Parameters
- $order : TOrderGraph
- $noteKey : string|null
Return values
string|nullwatermarkInvoicedStatus()
Returns `null` on success, otherwise a transient failure (see below).
private
watermarkInvoicedStatus(Order $order, OrderStatus $status) : Result|null
Moves the watermark orders.invoiced_status = status.id after a successful issue of a new
document and also when an existing one was found (the status is invoice-handled in both cases).
IMPORTANT: the write goes through OrderManager::updateColumns() of a manager created lazily by
OrderManagerFactory, never an injected OrderManager. OrderManager::completedSave() calls this
class back (regenerateOrderDocument()), so injecting the manager directly would create a DI cycle
(Manager, Service, Manager).
Parameters
- $order : Order
- $status : OrderStatus