Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Ceny, měny a formát částek

Ceny, měny a formát částek#

Jak se částka dostane z čísla do textu: v jaké měně, s jakými oddělovači, s jakou jednotkou a v jakém jazyce. Kapitola je pro toho, kdo vypisuje cenu na webu, v e-mailu nebo na dokladu, nebo kdo čte cenu od uživatele.

Měna na webu = platební region#

Katalog, košík i objednávka ukazují ceny v měně platebního regionu, ne v měně lokalizace. Region vybírá DisplayRegionResolver::resolve() v tomto pořadí:

Pořadí Region
1 region, který zákazník zvolil v objednávce (checkout, session košíku)
2 region lokalizace (/en/ → region „EU“ → EUR)
3 první použitelný výchozí region (is_default)
4 první použitelný region vůbec

Použitelný = aktivní region s aktivní měnou, která není interní. Region s vypnutou nebo interní měnou (kredity) by ukázal ceny, za které se nedá zaplatit, proto se přeskočí.

Front si region nastaví v CurrencyInitTrait::initializeDisplayRegion() (katalog vždy podle lokalizace, bez kroku 1); košík a checkout ho mohou přepnout volbou zákazníka (CheckoutRegionTrait). Šablona dostane měnu jako $currency a funkci pageCurrency().

?curr= v adrese front ignoruje

Jediný zdroj měny je resolver. Kdo by přidal přepínač měny přes parametr adresy, nic se nezmění — a zákazník bude dál vidět ceny v měně regionu.

Jiný region než lokalizace = zpráva zákazníkovi

DisplayRegionResolver::differsFromLocalization() řekne, že se objednávka počítá v jiném regionu než katalog. Košík je pak přepočtený (jiná měna nebo DPH) a zákazník o tom musí dostat hlášku.

Formát částky: lokalizace před měnou#

Filtr priceFormat (PriceLatteExtension) skládá formát v PriceFormat::resolve() ze dvou zdrojů po jednotlivých polích: lokalizace (cms_system_localizations) má přednost v polích, která má vyplněná (null = „neřeším“), měna je trvalý záložní zdroj. Stejné euro je tak „12,10 €“ na /cs/ a „€12.10“ na /en/.

Pole Co řídí
separator, decimal_separator, precision tisíce, desetinná čárka/tečka, počet míst
currency_symbol_position a za částkou („12 500 €“), b před ní („€12.10“)
negative_symbol_position b mínus před číslem, a za ním
show_suffix, suffix přípona celé částky (,-); kredity ji mají vypnutou → „100 krd“

Mezera mezi symbolem a číslem: za částkou vždy, před částkou jen u písmenné zkratky; znak (€, $) se před číslo lepí bez mezery.

Přípona ,- a desetinná místa#

,- znamená „a nula haléřů“, takže se s vypsanými desetinnými místy vylučuje (PriceHelper::convertByLocalization()):

Částka Výsledek
celá (50) desetinná místa zmizí, přípona zůstane: 50,-
necelá (1234.56) desetinná místa zůstanou, přípona zmizí: 1 234,56
přípona vypnutá (měna, nebo \|priceFormat:$currency, '') vždy precision desetinných míst

Jednotka za částkou#

Za částku jde přeložený a pluralizovaný popisek měny (cms_system_currency_texts.unit_label), jinak zkratka měny. Popisek je vzor Symfony intervalů:

{0}kreditů|[1,4]kredity|[5,Inf]kreditů

CurrencyUnitFormatter ho rozbalí přes IdentityTranslator podle hodnoty částky; explicitní intervaly nezávisí na jazyce. Chybí-li překlad pro jazyk, vrátí null a použije se zkratka.

Vzor nepouštěj přes translator aplikace

Contributte translator má returnOriginalMessage a neznámý „klíč“ vrátí beze změny — na stránce by se objevil celý vzor {0}kreditů|[1,4]….

Formát symbolu žije na dvou místech

Front JS (formatPrice() v assets/js/components/i18n.js, vlastní kopie v každé frontové šabloně) skládá částku stejně jako PriceFormat::attachSymbol(). Změníš-li pravidlo jen v PHP nebo jen v některých šablonách, cena z PHP a cena dopočítaná v JS se na jedné stránce rozejdou („12,10 €“ vedle „€12,10“).

Doklady a e-maily: formát zákazníka, ne požadavku#

Doklad ani e-mail se nesmí formátovat podle toho, kdo ho zrovna generuje (admin v češtině, cron bez jazyka). Kdyby ano, admin s přepnutou lokalizací by přegeneroval tři roky starou fakturu a čísla v ní by vyšla jinak než v původním PDF.

Doklad si proto lokalizaci a jazyk ukládá při vzniku a generátor je na dobu renderu vloží do PriceFormatContext. priceFormat, approxPrice i datumové filtry čtou nejdřív odsud a teprve pak lokalizaci požadavku. Šablony PDF žijí v databázi a nemění se.

Metoda Kdy Co navíc
overrideForDocument($localization, $languageId) PDF dokladu (InvoicePdfGenerator) režim účetního dokladu: pevná desetinná místa a žádná přípona ,- na žádné částce
runWith($localization, $languageId, $render) e-mail objednávky (OrderEmailGenerator) po renderu vrátí předchozí stav (ne reset()), takže přežije vnější override
override(…) + reset() ostatní jen lokalizace a jazyk

Režim dokladu platí pro všechny typy (objednávka, faktura, proforma, storno, dobropis) — jdou jedním generátorem. Přípona by na dokladu míchala sloupec: 1.000,- nad 210,00.

Texty a čísla dokladu podle zákazníka skládá DocumentLocale:

Co Podle čeho
texty (NEON, locale translatoru) jazyk objednávky/dokladu; bez jazyka lokalizace; jinak cs_CZ
datum short_date_format lokalizace; prázdný = d.m.Y
číslo (množství, poplatek) oddělovače lokalizace a měny; celé číslo bez desetinných míst, desetinné bez koncových nul (Quantity, filtr \|quantity)
chybějící překlad čeština — holý klíč //document… na doklad nikdy nejde

Postup: vykresluji doklad nebo e-mail zákazníkovi#

  1. Načti lokalizaci a jazyk z objednávky nebo dokladu, ne z požadavku.
  2. Obal render do overrideForDocument() (doklad) nebo runWith() (e-mail).
  3. U override…() vrať stav ve finally:

    $this->priceFormatContext->overrideForDocument($localization, $invoice->getLanguageId());
    try {
        $abs = $this->pdfGenerator->generate(...);
    } finally {
        $this->priceFormatContext->reset();
    }
    
  4. Formátuj přes PriceLatteExtension (priceFormat), ne přes vlastní helper — jen extension zná override a režim dokladu.

Bez finally přeteče formát do dalšího dokladu

Služba je jedna na DI kontejner. Výjimka uprostřed renderu nechá override nastavený a hromadné generování naformátuje druhý a další doklad podle prvního. Na webu kontejner zanikne s požadavkem, v cronu a CLI žije dál.

Přibližný přepočet: approxPrice#

ApproximatePrice přepočítá částku ručním kurzem cms_system_currencies.exchange_rate („100 000 Kč (≈ €4,100)“). Je to jen orientační doplněk: žádná cena se jím neukládá ani nefiltruje.

Kurz znamená cizí měnu za 1 CZK (CZK = 1, EUR = 0.041) — stejně jako Store a SMS brána. Z měny A do měny B je tedy částka / kurzA × kurzB. Při vypsání se výsledek od 10 jednotek zaokrouhlí na celé (WHOLE_UNITS_FROM), pod tím na setiny.

Přepočet vrátí null (a filtr prázdný řetězec), když:

  • částka chybí nebo není kladná,
  • jde o tutéž měnu,
  • některá strana není peněžní měna (monetary = 0, kredity); interní peněžní měna se přepočítá,
  • kurz chybí nebo je nulový.

Obrácený kurz obrátí každou přepočtenou cenu

Kdo kurz zadá jako „Kč za 1 €“ (24,4 místo 0,041), dostane u každé přepočtené ceny částku zhruba šestisetkrát větší. Nic nespadne a nic se nezaloguje; chybu uvidí až návštěvník u ceny.

Číslo od uživatele: Decimal#

Decimal::fromInput() je jediné místo, které říká, jak vypadá „číslo od uživatele“: desetinná čárka i tečka, tisícové oddělovače včetně nezlomitelných mezer (U+00A0, U+202F). Vrací numeric-string v tečkovém tvaru, nebo null.

Vstup Výsledek
1 234,5 '1234.5'
'', samé mezery null
1 000 Kč null

Postup: přidávám desetinné pole do formuláře#

  1. Na pole dej masku (Form::PATTERN), aby uživatel nečíselný vstup viděl jako chybu.
  2. Hodnotu převeď Decimal::fromInput(), ne vlastní str_replace — kopie se rozejdou a tentýž vstup pak v každém formuláři dopadne jinak.
  3. O výchozí hodnotě rozhodni sám: ?? '1' u množství, ?? '0' u ceny.
  4. Nezaokrouhluj v helperu. Počet míst je vlastnost sloupce (decimal(10,3) u ceny, decimal(8,2) u množství dokladu fakturace, decimal(10,3) u skladového množství), ne vstupu. Na sloupec zaokrouhluje half-up (od nuly) až ten, kdo zapisuje (ColumnValue, sklad DocumentService).

null není nula

Kdyby se null převedlo na 0, zmizí rozdíl mezi nevyplněným a nulovým polem — a bez masky by se nečíselný vstup tiše uložil jako výchozí hodnota.

Kam sáhnout#

Chci Kde
výběr regionu a měny webu app/Core/Utils/Helpers/DisplayRegionResolver.php, app/Core/Traits/Front/Presenters/CurrencyInitTrait.php
skládání formátu app/Core/Utils/ValueObjects/PriceFormat.php
přípona a desetinná místa app/Core/Utils/Helpers/PriceHelper.php
filtr priceFormat app/Core/Latte/Extensions/PriceLatteExtension.php
jednotku za částkou app/Core/Utils/Helpers/CurrencyUnitFormatter.php
formát dokladu a e-mailu app/Core/Utils/Contexts/PriceFormatContext.php, app/Core/Utils/Helpers/DocumentLocale.php
přibližný přepočet app/Core/Utils/Helpers/ApproximatePrice.php, app/Core/Latte/Extensions/ApproxPriceLatteExtension.php
číslo od uživatele app/Core/Utils/Helpers/Decimal.php

Navazující kapitoly: Šablony Latte · E-shop · Fakturace · Odesílání e-mailů · Formuláře

Nastavení cookies