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ů:
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#
- Načti lokalizaci a jazyk z objednávky nebo dokladu, ne z požadavku.
- Obal render do
overrideForDocument()(doklad) neborunWith()(e-mail). -
U
override…()vrať stav vefinally: -
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#
- Na pole dej masku (
Form::PATTERN), aby uživatel nečíselný vstup viděl jako chybu. - Hodnotu převeď
Decimal::fromInput(), ne vlastnístr_replace— kopie se rozejdou a tentýž vstup pak v každém formuláři dopadne jinak. - O výchozí hodnotě rozhodni sám:
?? '1'u množství,?? '0'u ceny. - 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, skladDocumentService).
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