CoolCMS API

CompanyInvoiceGenerator
in package

FinalYes

Translates the selected work sheets of a job into an {@see InvoiceComposer} request.

A job is stored as WorkList (historical class name); a work sheet lives inside a job. All invoice knowledge (seller/customer snapshot, billing line, PDF variant matrix, calculator, PDF + QR) sits in Invoicer; this class only says "these work items, for this customer, for this company". Another module must not rely on the e-shop library or build its own copy of invoice composition.

Contract:

  • Pure builder. No write, transaction, lock or number from a billing line. The returned invoice is not saved: the caller (WorkListManager::invoiceSheets()) saves it under the job lock after the sheets move to closing, so every build error happens while the sheets are still open and bad input never leaves them half-processed.
  • No idempotency (see the composer contract). Two calls make two invoices; whether the sheets were already invoiced is decided by WorkListManager::invoiceSheets().
  • Guards are specific. A missing seller, invoice type, currency or customer and an unusable work item return Result::failure() with an errorCode and a message that names what to fill in. Invoice type and currency resolve through the chain job -> customer billing profile -> company settings; the guard runs after the whole chain, so the message names all three places. Per-item VAT (WorkItem::getVatPercents()) bypasses the guards: null tells the composer to use the request default.
  • One line per work item. Hourly work: quantity = actual hours, unit = hour, price = hourly rate (the calculator multiplies). A fixed price (manual_price) wins: quantity 1, unit piece, price = fixed price. Totals are not sent; the Invoicer calculator is authoritative.
  • N sheets make one invoice. The customer is on the job, not on the sheet. The line description carries the sheet name; name stays the item title and code stays empty. Without it the items of three sheets would merge into one indistinguishable column and the customer could not see which batch they pay for.
  • The caller selects and orders. The generator neither filters sheets and items (the filter is in WorkItemManager::findBillableForSheets()) nor reorders them. If both did it their own way, the invoice would differ from what the admin confirmed in the invoice modal, which is the worst kind of error with money: a silent one.

IMPORTANT: until cms_mod_invoicer_invoice_items.quantity is migrated to decimal(8,2), DecimalQuantityGuardTrait in InvoiceManager::beforeSave() rejects fractional hours. The generator deliberately neither bypasses it nor rounds hours, because rounding would silently change the invoice amount (2.5 h -> 3 h = +250 at a rate of 500). Whole hours pass either way; WorkListManager::invoiceSheets() turns the rejection into a readable message.

Table of Contents

Constants

UNIT_HOUR  : string = 'hod'
Unit of hourly work. Fixed in Czech for now; the invoice language is not resolved yet.
UNIT_PIECE  : string = 'ks'
Unit of fixed-price work: one piece of the delivered service.

Properties

$composer  : InvoiceComposer
$invoicerCustomerManager  : CustomerManager

Methods

__construct()  : mixed
generate()  : Result
Builds an unsaved invoice for the work items of the selected work sheets of one job.
buildLine()  : InvoiceComposerLine|Result
Maps one work item to one invoice line. Returns a `Result` failure when a human must fix the item.
buildRequest()  : InvoiceComposerRequest|Result
Runs the header and line guards and builds the composer request. Returns a `Result` on invalid input.
failure()  : Result
Builds a failure `Result` carrying `errorCode` in its data (composer contract).
sheetLabel()  : string
Returns the work sheet label for the line `description`.

Constants

UNIT_HOUR

Unit of hourly work. Fixed in Czech for now; the invoice language is not resolved yet.

public string UNIT_HOUR = 'hod'

UNIT_PIECE

Unit of fixed-price work: one piece of the delivered service.

public string UNIT_PIECE = 'ks'

Properties

Methods

generate()

Builds an unsaved invoice for the work items of the selected work sheets of one job.

public generate(Company $company, WorkList $list, array<int, WorkItem>}> $sheetsWithItems) : Result

Success: a Result with an Invoice entity in its data (composer contract). Failure: Result::failure() with ['errorCode' => ...]; the codes are permanent (a configuration or data defect that retrying does not fix):

  • customerMissing - the job has no customer (an internal job cannot be invoiced),
  • sellerMissing - the company has no invoicer_seller_id (invoice header),
  • invoiceTypeMissing - neither job, customer nor company has an invoice type,
  • currencyMissing - neither job, customer nor company has a currency,
  • noLines - empty list of work items (the caller reports it as nothingToInvoice),
  • invalidWorkItem - the price cannot be computed (missing rate or hours),
  • plus any code of InvoiceComposer::compose() (e.g. numberSourceMissing).

The input is N work sheets of one job, not a flat list of items: the caller passes [['sheet' => ..., 'items' => [...]], ...] already ordered by work_sheets.order and within a sheet by work_items.order. The generator neither reorders nor filters. The sheet a line belongs to is carried by its description (the sheet name).

Parameters
$company : Company

company of the job; carries the invoicing identity. The templates are covariant on purpose: only scalar fields are read (seller, invoice type, currency, billing line, VAT rate), never translations or gallery, so it does not matter which layer the company came from; an invariant <object, object> would force pointless casts

$list : WorkList
$sheetsWithItems : array<int, WorkItem>}>

billable items per sheet, selected by the caller (billable = 1 and deleted = 0 and price != 0); the generator does not filter them, so the invoice cannot differ from what the invoice modal showed

Return values
Result

buildLine()

Maps one work item to one invoice line. Returns a `Result` failure when a human must fix the item.

private buildLine(WorkItem $item, WorkSheet $sheet) : InvoiceComposerLine|Result

A fixed price wins over the calculation, exactly as WorkItemManager::computePrice() does. If the two diverged, the invoice would state a different amount than the price column the admin summed in the grid.

actual_hours goes to quantity as the decimal string from the database ("2.50"), not as a float: a float conversion is a needless risk with money, and both the composer and the entity accept strings (DecimalQuantityTrait).

description is the work sheet name. On an invoice built from N sheets it is the only hint of which batch the item comes from; name is the item title and code stays empty.

The line supply kind is work_items.supply_kind_id; when null the composer uses the company default (companies.default_supply_kind_id, sent in the request header). The VAT engine derives the rate from it on the supply date, so vat applies only where neither the item nor the company gives a supply kind.

Per-line VAT: WorkItem::getVatPercents() goes to InvoiceComposerLine::$vat without any chain; null tells the composer to use the request default, which already carries the customer -> company chain (see vatPercents in InvoiceComposerRequest::create()).

Parameters
$item : WorkItem
$sheet : WorkSheet
Return values
InvoiceComposerLine|Result

buildRequest()

Runs the header and line guards and builds the composer request. Returns a `Result` on invalid input.

private buildRequest(Company $company, WorkList $list, array<int, WorkItem>}> $sheetsWithItems) : InvoiceComposerRequest|Result

Kept apart from generate() on purpose: building the request is a pure function over entities (no database, no DI), whereas compose() touches the Invoicer directory. So all local logic (line order, description, mapping of hours and fixed price) can be verified without a container: tests/Manual/Company/w4_generator_check.php.

Parameters
$company : Company
$list : WorkList
$sheetsWithItems : array<int, WorkItem>}>
Return values
InvoiceComposerRequest|Result

failure()

Builds a failure `Result` carrying `errorCode` in its data (composer contract).

private failure(ResultCode $code, string $errorCode, string $message) : Result
Parameters
$code : ResultCode
$errorCode : string
$message : string
Return values
Result

sheetLabel()

Returns the work sheet label for the line `description`.

private sheetLabel(WorkSheet $sheet) : string

An empty sheet name is replaced by a deterministic placeholder: an invoice line without any hint of its sheet is worse than a fallback number.

Parameters
$sheet : WorkSheet
Return values
string
On this page

Search results