Dates, currency and signs

TL;DR

Which date field leads, how signed amounts are combined, and the conventions that make an aggregate correct.

Choose the date basis, currency and sign convention before comparing or adding figures.

Dates: creation, transaction and accounting period

On the seven transaction tables, createddate is the leading creation field. It is written on every import. Transaction listing filters use it; customer numbering and rule activation use trandate, the impact listing filters on trandate, and ledger reports use accounting periods.

createddatetime holds the same moment with a time. For records imported after it was introduced, its date part always equals createddate. Records imported earlier carry no timestamp at all until someone backfills them, so any surface reading it falls back to createddate.

The exception that matters: customer order numbering does not use createddate. It orders by trandate, then by sales order id as a tiebreak. A customer's first order is the earliest by transaction date, which is not necessarily the earliest by creation date. A screen filtered on creation date and a sequence computed on transaction date will disagree about which order came first, and both are behaving correctly.

Rule activation windows are also evaluated against the transaction date, inclusive at both ends.

The screen defaults are configured, not hardcoded. The transaction-style pages open on pages.transactionsfrom to pages.transactionsto, today minus three months to yesterday, with pages.rowlimit rows. Both the page and the table behind it read the same file, so they cannot disagree. See configuration and boundaries.

Signs: preserve the input, then check the display

CCC adds signed amounts; the engine does not turn a value negative because its account is a cost account. A fixed impact keeps the configured sign. A percentage multiplies the signed field by the signed percentage. A GL impact keeps the selected posting-line amount.

A transaction header and its posting lines need not use the same sign convention. Do not infer the sign of revenue impacts from an invoice header, or the sign of every cost from a refund header. The shipping seed, for example, supplies a positive fixed cost. The refund payment seed multiplies a negative credit-memo header by a negative rate, also producing a positive cost.

Synthetic posting example:

ComponentStored amount
revenues-100.00
cogs+60.00
voc+5.00
vmc+10.00
gp = revenues + cogs-40.00
cm1 = gp + voc-35.00
cm2 = cm1 + vmc-25.00

On a display that reverses these signs, CM2 is +25.00, and adding another +2.00 stored cost reduces that displayed margin to +23.00. A positive stored cost is therefore not evidence of a misconfigured rule, and a negative stored CM2 alone is not evidence of a loss.

To validate a rule, record the signed base, rate or fixed amount, resulting impact, and the screen's sign setting. Use the same convention on both sides of a comparison. A refund can reverse revenue and still incur a new processing cost; those are separate impacts.

Currency

Euro amounts are supplied or derived per transaction. The import retains upstream euro and foreign amounts and the exchange rate; it does not look up a current rate when a report runs. Prepayment classification explicitly computes round(payment * exchangerate, 2).

ValueHow to read it
amount_eur_exvat, amount_eur_invatAlready in euro; do not convert again
amount_foreign_invatOriginal-currency header amount where the type carries it
exchangerateRate retained on the transaction
currency_idCurrency of the original amount

Do not sum foreign amounts across currencies. Current catalogue prices and commercetools discount amounts have their own units and are not replacements for a transaction's recorded euro amounts. The income statement's scaling and sign inversion change presentation, not storage.

NULL and zero are different

They are not interchangeable anywhere in CCC, and the difference is load-bearing in at least one place: in the customer order sequence, 0 means an order that exists but is not counted, while NULL in the comparison table means the old pipeline never numbered it. Reading either as "no value" produces a wrong count.

Per-column meaning lives in the dictionary at /dictionary, not in prose.

Evidence

createddate leading and the createddatetime fallback from AGENTS.md, Models and database. Numbering basis from app/models/customers.php:1190-1192, ORDER BY n.trandate ASC, n.salesorder_id ASC. Signed aggregation from app/models/impact.php::getimpacttransaction(), unchanged fixed and GL amounts from app/models/calculationrules.php::calculate_buildimpact(), positive shipping costs from config/seed/cm_calculationrules/shipping.csv and app/jobs/install/seed_cm_calculationrules.php. Currency conversion happening per transaction against a stored rate from the exchangerate column on each transaction table and its use in classify->classifyprepayment(), which computes the euro amount as the paid amount times that rate. Basis: code-checked against the current implementation.

About this document
  • Type
    concept
  • Status
    active
  • Updated
    2026-09-14
  • Created
    2026-09-13