CoolCMS API

OrderInvoiceGenerator
in package

FinalYes

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
phpstan-import-type

TOrderGraph from OrderCalculator

TCreditNoteLedger from CreditNoteLedger

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'

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

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'

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'

Properties

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|null

generateForStatus()

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
Result

generatePartialCreditNote()

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
Result

markPaid()

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
Result

regenerateOrderDocument()

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
Result

ancillaryLineName()

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
string

appendCreditNoteVouchers()

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 computes base x percents/100 from 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 as appendVouchers() uses for $lastPercentIndex.
Parameters
$invoice : Invoice
$source : Invoice
$lines : array<string, array<int, int>>
$ledger : TCreditNoteLedger
$isFinal : bool

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
$invoice : Invoice
$order : Order

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
$invoice : Invoice
$order : Order

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/100 from 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
$invoice : Invoice
$order : Order

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
$invoice : Invoice
$order : Order
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
Invoice

buildCreditNoteWithinLimit()

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]

chargeableBase()

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-string

checkParity()

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
$order : Order
$invoice : Invoice
$warnings : array<int, string>
Return values
bool

checkParityUnlessCreditNote()

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
bool

combinedDiscountPercent()

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
float

creditNoteTypeId()

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|null

documentLocale()

private documentLocale(TOrderGraph $order) : string
Parameters
$order : TOrderGraph
Return values
string

documentText()

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
string

documentUnit()

private documentUnit(string|null $code, TOrderGraph $order) : string
Parameters
$code : string|null
$order : TOrderGraph
Return values
string

effectiveVatPercent()

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
float

eshopInvoicerKey()

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|null

exhaustsSource()

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
bool

existingInvoice()

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|null

feeDescription()

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|null

findOrderDocument()

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|null

findSourceInvoiceForOrder()

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|null

hasLegacyFeesInPrice()

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
bool

localizationRef()

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|null

mirrorLine()

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
$from : InvoiceItem|InvoiceShipper|InvoicePaymentMethod
$to : InvoiceItem|InvoiceShipper|InvoicePaymentMethod

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-string

negatePrice()

private negatePrice(string $price) : string
Parameters
$price : string
Return values
string

nonCreditInvoiceTypeIds()

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|null

orderDocumentTypeId()

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|null

orderLocalization()

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|null

percentOf()

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-string

percentShare()

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-string

remainingLines()

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|null

round3()

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-string

settingInt()

private settingInt(string $key, int $default) : int
Parameters
$key : string

module 7 key (invoice.default.*)

$default : int
Return values
int

settingIntOrNull()

private settingIntOrNull(string $key) : int|null
Parameters
$key : string

module 7 key (invoice.default.*)

Return values
int|null

taxIdForInvoice()

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|null

totalRequestedQuantity()

private totalRequestedQuantity(array<string, array<int, int>> $lines) : int
Parameters
$lines : array<string, array<int, int>>
Return values
int

translateTaxNote()

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|null

watermarkInvoicedStatus()

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
Return values
Result|null
On this page

Search results