EdeeShop · Implementace

Výpočet ceny objednávky a rozpočítání slev

Upraveno

Kapitola popisuje, jak kalkulátor objednávky (com.fg.cps.eshop.order.service.calculator) počítá doklad, částku k úhradě a čerpání cenových politik a jak rozpočítává slevy mezi položky. Navazuje na kapitoly Cenové politiky a Zaokrouhlení částky k úhradě.

Kalkulátory

KalkulátorKlíč (calculatorKey)Kdy se použijeVýpočet DPH
DefaultOrderCalculatorfromPriceWithTaxCalculatorvše kromě B2B (B2C, nepřihlášený zákazník, uživatel bez formy, bez web kontextu)„shora“ – zdrojem je cena s DPH, základ se dopočítá
FromPriceWithoutTaxOrderCalculatorfromPriceWithoutTaxCalculatorB2B zákazník (UserForm.LEGAL_ENTITY)„zdola“ – zdrojem je základ, DPH se počítá za sazbu z dokladu

Kalkulátor vybírá OrderCalculatorProviderDefaultImpl (lze nahradit vlastní implementací OrderCalculatorProvider). Klíč kalkulátoru se ukládá k objednávce a přepočty objednávky (úpravy, expedice, import) používají uložený kalkulátor.

Oba kalkulátory sdílí AbstractOrderCalculator a liší se ve výpočtu cen řádku (calculatePriceWithTax, calculatePriceWithoutTax) a příznakem isPriceWithTaxDerivedFromPriceWithoutTax():

  • shora: cena řádku s DPH = round(množství × jednotková cena s DPH, 2), základ se z ní dopočítá,
  • zdola: základ řádku = round(množství × jednotkový základ, 2), cena s DPH se odvodí ze základu.

Kalkulátor pracuje s cenami řádků (součty za řádek), slevy se rozpočítávají mezi řádky, ne mezi kusy. Jednotková cena po politikách (getUnitPriceAfterPricePolicies, používá ji např. PartOrderCalculator) vzniká až z ceny řádku – při ladění dílčí expedice je to první místo, kde haléře nemusí sedět.

Pojmy

  • Doklad – celková cena s DPH spočítaná za sazby DPH přes všechny položky kalkulace (produkty, doprava, platba). U výpočtu zdola je to za každou sazbu round(součet základů × (1 + sazba), 2), u výpočtu shora součet cen s DPH (AbstractOrderCalculator#getDocumentTotalWithTax). Z dokladu se počítá částka k úhradě a čerpání politik. Uložená celková cena (totalPrice) je v běžných případech shodná, liší se jen v režimu SINGLE a u položek s nekladnou cenou (viz Známá omezení).
  • Částka k úhradě před zaokrouhlením (totalPriceToPayBeforeRounding) = doklad − čerpání úhrad (PAYMENT), nikdy záporná.
  • Částka k úhradě (totalPriceToPay) – zaokrouhlená přes RoundingStrategy na rounding objednávky (CalculationSource#getRounding, určený konfigurací, viz RoundingScale; typicky CZK 0 = celé koruny, EUR 2). Výchozí strategie zaokrouhluje nahoru (CEILING). Rozdíl je řádek zaokrouhlení (totalPriceToPayRounding). Do zaokrouhlení se nikdy nepřesouvají rozdíly ze slev.
  • Čerpání politiky (CalculationPolicy#getAbsoluteAmount) – o kolik politika skutečně snížila doklad (u slev), resp. kolik se z ní uhradilo (u PAYMENT). Z čerpání vychází stržení kreditu (CreditChargeInterceptor, při importu CreditOrderImportPopulator) a jeho vratka při úpravě objednávky (CreditOrderActuator), řádky úhrad na faktuře (InvoiceGeneratorImpl) a dělení objednávky (PartOrderCalculator).

Pořadí výpočtu

  1. Politiky se uplatní v pořadí priority (od nejvyšší), neplatné se přeskočí. Každá změní ceny položek kalkulace, další politika už počítá ze zlevněných cen.
  2. Po každé slevě (ABSOLUTE_ORDER, PERCENT_ORDER) nastaví AbstractOrderCalculator#applyPolicy čerpání na skutečné snížení dokladu; u ABSOLUTE_ORDER s nezáporným nominálem nejvýše nominál a nikdy záporné. Záporný nominál (přirážka) si znaménko ponechá.
  3. PAYMENT (dárkový poukaz, kredit jako platba) ceny položek ani DPH nemění. Čerpá nejvýše nominál a nejvýše hodnotu dokladu dotčených položek sníženou o součet všech již zpracovaných úhrad (bez ohledu na to, na které položky mířily) – PaymentCalculationStrategy.
  4. Po uplatnění všech politik omezí capPaymentsToDocument čerpání úhrad v pořadí zpracování tak, aby jejich součet nepřesáhl doklad – i když po úhradě následuje sleva (priority jsou konfigurovatelné).
  5. Spočítají se součty: doklad, částka k úhradě, částka k úhradě za položky (bez dopravy a platby, stejným výpočtem jako doklad) a zaokrouhlení.

Absolutní sleva (AbsoluteCalculationStrategy)

Sleva ABSOLUTE_ORDER se rozpočítává do cen položek (ne jako samostatný řádek). Je to nutné kvůli správnému DPH při více sazbách a kvůli vratkám, dílčím objednávkám a expedicím, kde položka nese svou zlevněnou cenu.

Zlevňují se položky objednávky (bez dopravy a platby), případně jen položky vybrané ve filteredItemId; položky s nekladnou cenou se neslevňují. Položky se zpracují v pořadí dle sazby DPH. Výše slevy je nejvýše hodnota zlevňovaných položek, případně ji omezí zdrojová cena (AbstractCalculationParameterWithSourcePrice).

Společný princip

  • Díl slevy každé položky je poměrný k jejímu základu a zaokrouhlí se matematicky.
  • Cena položky po slevě je vždy v intervalu 0 až cena před slevou – sleva položku nikdy nezdraží ani neudělá zápornou.
  • Zbytek po zaokrouhlení dostane poslední položka, která jej pojme; co nepojme, dostanou předchozí položky odzadu.
  • Je-li nominál alespoň menší z hodnot „součet cen zlevňovaných položek s DPH“ a „doklad zlevňovaných položek“ (getDiscountAmount), všechny se rovnou vynulují a čerpání je skutečné snížení dokladu. U výpočtu zdola se obě hodnoty mohou lišit o haléř – proto třeba kredit 159,79 vynuluje položky 99,90 + 59,90 (součet 159,80, doklad 159,79).
  • Přirážka (záporný nominál) se rozpočítá stejně, jen žádná položka nezlevní a shora omezená není.

DPH shora (B2C)

Sleva se rozpočítá postupně: díl položky = zbývající sleva × podíl jejího základu na základu zbývajících položek. Nová cena s DPH se zaokrouhlí na rounding objednávky (typicky CZK celé koruny, EUR centy) a omezí do intervalu 0 až cena před slevou; co se tím nerozpočítalo, zůstává ve zbývající slevě. Základ se dopočítá z ceny s DPH. Doklad se sníží přesně o slevu.

DPH zdola (B2B)

DPH se zaokrouhluje za celou sazbu dokladu (včetně nezlevňovaných položek, dopravy a platby), proto se počítá po sazbách:

  1. Sleva se rozdělí mezi sazby zlevňovaných položek poměrně k jejich základům. Díl sazby je nejvýše to, o kolik může doklad sazby klesnout (nezlevňované položky sazby si svou část dokladu ponechají).
  2. Cíl sazby = doklad sazby před slevou − díl slevy sazby + haléře, o které předchozí sazby zůstaly pod svým cílem.
  3. Dopočítá se nejvyšší součet základů zlevňovaných položek sazby, při kterém doklad sazby cíl nepřesáhne (getHighestBaseWithinTarget).
  4. Snížení základu se rozpočítá mezi položky sazby poměrně k základům a cena s DPH se odvodí ze základu.

Doklad tak nikdy nepřesáhne „doklad před slevou − sleva“ a je nejvýše o haléř nižší (ve prospěch zákazníka). Čerpání je nominál. Základy položek po slevě jsou v haléřích; na celé koruny se zaokrouhluje jen částka k úhradě.

Výjimka: pokud je sleva vyšší, než o kolik mohou zlevňované položky doklad snížit (součet limitů sazeb z bodu 1, typicky sleva o haléře nižší než hodnota položek), zlevňované položky se vynulují a čerpání je skutečné snížení dokladu (o haléře nižší než nominál).

Procentní sleva (PercentCalculationStrategy)

  • Počítá se po položkách (řádcích) z aktuální ceny řádku, případně ze zdrojové ceny (sourceUnitPrice × množství). Sleva s DPH i bez DPH se spočte zvlášť a zaokrouhlí na discountRounding z parametrů politiky.
  • Procento se převádí discount / 100 se zaokrouhlením na 2 desetinná místa – např. 12,5 % se počítá jako 13 %.
  • Výsledná cena se znovu normalizuje kalkulátorem; u výpočtu zdola se cena s DPH odvodí ze základu, sleva s DPH proto není přesně X % z ceny s DPH. Příklad 10 % na 224 + 44 Kč: B2C 202 / 40, doklad 242, čerpání 26,00; B2B 201,01 / 39,16, doklad 240,16, čerpání 27,83.
  • S applyOnlyIfLower se sleva na položku nepoužije, pokud by cenu nesnížila. Sleva nad 100 % vyhodí IllegalArgumentException.
  • Čerpání je snížení dokladu, nominálem omezené není.

Záruky

Ověřují je testy (AbstractOrderCalculatorTest#assertDocumentConsistent, assertNoItemRaised, assertNoItemNegative):

  1. Částka k úhradě před zaokrouhlením = doklad − čerpání úhrad, nikdy záporná.
  2. Absolutní sleva nižší než hodnota zlevňovaných položek se vyčerpá celá (čerpání = nominál) a zákazník nezaplatí víc než cenu sníženou o celý nominál: u výpočtu shora doklad klesne přesně o nominál, u výpočtu zdola o nominál až nominál + 1 haléř. Výjimky: sleva omezená zdrojovou cenou a u výpočtu zdola sleva vyšší než součet limitů sazeb (viz výše) – tam je čerpání skutečné snížení dokladu.
  3. Žádná položka po absolutní slevě nezdraží ani není záporná (u přirážky žádná nezlevní).
  4. Čerpání úhrad (PAYMENT) v součtu nepřesáhne doklad.

Příklady

Všechny příklady jsou v CZK se sazbou 21 %, rounding 0 a zaokrouhlením částky k úhradě nahoru na celé koruny. Ceny jsou zadané s DPH; u výpočtu zdola je zdrojem základ (uvedený v závorce).

Jedna položka

Položka 160 Kč s DPH (základ 132,23), sleva 15 Kč.

B2C (shora)B2B (zdola)
Položka po slevě (s DPH / základ)145,00 / 119,83144,99 / 119,83
Doklad145,00144,99
K úhradě před zaokrouhlením / zaokrouhlení145,00 / 0144,99 / 0,01
K úhradě145145
Čerpání slevy15,0015,00

U B2B dá základ 119,84 doklad 145,01 a zákazník by zaplatil 146 Kč; proto se volí nejvyšší základ, při kterém doklad nepřesáhne 160 − 15 = 145, tj. 119,83 (144,99). Haléř jde ve prospěch zákazníka a dorovná jej zaokrouhlení.

Placená doprava ve stejné sazbě (B2B)

Položka 160 (základ 132,23), doprava DPD 89 (základ 73,55), dobírka (základ 40,50), sleva 15 jen na položku.

  • Doklad před slevou: (132,23 + 73,55 + 40,50) × 1,21 = 298,00; cíl 298,00 − 15 = 283,00.
  • Nezlevňované základy sazby 114,05; nejvyšší součet základů s dokladem ≤ 283,00 je 233,88 (233,89 by dal 283,01) → položka 119,83.
  • Doklad 282,99 (základ 233,88, DPH 49,11), k úhradě 283 Kč, zaokrouhlení 0,01, čerpání 15,00.

Kdyby se cíl počítal jen ze zlevňované položky, doprava by do zaokrouhlení DPH nevstoupila a doklad by mohl vyjít o haléř nad cílem.

Více položek

Položky 224 a 44 Kč s DPH (základy 185,12 a 36,36), sleva 15.

B2C (shora)B2B (zdola)
Položka 1 (s DPH / základ)211 / 174,38211,46 / 174,76
Položka 2 (s DPH / základ)42 / 34,7141,53 / 34,32
Doklad253,00252,99
K úhradě253253
  • B2C: díl položky 1 = 15 × 185,12 / 221,48 = 12,54 → cena 211,46 zaokrouhlená na korunu 211; zbývající sleva 2 dostane položka 2 (44 → 42).
  • B2B: doklad před slevou 267,99, cíl 252,99 → nejvyšší součet základů 209,08 (209,09 by dal 253,00); snížení základu 12,40 se rozdělí poměrně: 10,36 a 2,04.

Zbytek, který poslední položka nepojme (B2C)

Deset položek po 99,50 a jedna za 2,00 Kč, sleva 0,60. Ceny se zaokrouhlují na celé koruny, takže první položka klesne na 99 (o 0,50 víc, než je její díl). Ostatní položky by zaokrouhlení zdražilo, proto zůstanou na 99,50. Zbytek 0,40 by zdražil poslední položku (2,00 → 2,40), proto jej odzadu převezme druhá položka (99,50 → 99,40). Doklad 996,40 = 997,00 − 0,60, žádná položka nezdražila.

Sleva pokrývající objednávku

Položky 149,90 a 89,90 Kč, kredit 10 000 Kč jako sleva. Položky se vynulují, doklad je 0 a nic se neplatí. Kredit se čerpá ve výši skutečného snížení dokladu: B2C i B2B 239,80; u položek 99,90 + 59,90 B2C 159,80, B2B 159,79 (doklad za sazbu).

Úhrada před slevou

Položka 500 Kč, nejdříve dárkový poukaz 1000 Kč jako úhrada (vyšší priorita), poté sleva 15. Doklad 485, k úhradě 0, z poukazu se čerpá 485 (ne 500) – zbytek zůstává zákazníkovi.

Známá omezení

  • Poukaz typu SINGLE (CalculationSource#getVoucherTaxStrategy) odečítá úhradu z první (nejnižší) sazby přímo v totalPrice; u výpočtu zdola se totalPrice a částka k úhradě mohou lišit o haléř.
  • totalPrice vynechává položky s nekladnou cenou, doklad pro částku k úhradě je započítává. Záporné ceny položek ale nejsou podporovány (ceny produktů se validují na ≥ 0).
  • capPaymentsToDocument omezuje úhrady na celý doklad, nerespektuje excludeShipping/excludePayment úhrady.
  • Faktura (InvoiceGeneratorImpl) přičítá řádek zaokrouhlení do základu jedné sazby, takže rekapitulace DPH (základ × sazba) nemusí sedět s DPH na haléř. Nesouvisí s kalkulátorem.

Kde hledat při ladění

  • Kód: AbstractOrderCalculator (pořadí, doklad, čerpání, částky k úhradě), AbsoluteCalculationStrategy (rozpočítání absolutní slevy), PercentCalculationStrategy, PaymentCalculationStrategy. Důvody současného řešení a zamítnuté varianty: ADR Rozpočítání slev, eshop#3003.
  • Testy s přepočítanými příklady a popisem v Javadocu: DefaultOrderCalculatorTest (B2C), FromPriceWithoutTaxOrderCalculatorTest (B2B), společné kontroly v AbstractOrderCalculatorTest.
  • Uložená data objednávky: T_ESHOP_ORDER (calculatorKey, totalPrice*, totalPriceToPay*), T_ESHOP_ORDER_PRICE_POLICY (discount, absoluteAmount), T_ESHOP_ORDER_LINE (ceny položek před a po politikách).