Jak na ceny v Eshopu
Návod, jak programovat v PHP ceny v rámci eshopu
13. srpen 2010
Poslední aktualizace 12. 02. 2011 16:15:52

Rozhodl jsem se, že na tohle téma napíšu vlastní článek. Není to totiž nic jednoduchého určit, jakým způsobem se vlastně v kódu počítají ceny, jaká cena se zobrazuje a jaká se ve skutečnosti vypočítává.
V tomto článku se dočtete, jak správně postupovat a programovat při vypočítávání ceny, co je třeba vědět a čeho je třeba se vyvarovat. Vše v populárním open-source programovacím jazyce PHP, příručka ale může v lecčems poradit i programátorům v jiných programovacích jazycích na jiných serverech (ASP.NET, Java ...).

Pokud hodláte prodávat eshop, který neprovádí žádné výpočetní operace s cenou tak ani nemusíte pokračovat ve čtení tohoto článku, máte prostě uložené jedno číslo v databázi a to zobrazujete. Jednoduché.
Ale kdo by od vás takový eshop koupil, že ;) Maloobchodníci prodávájí za cenu s DPH a vy musíte pro ně to DPH dopočítat, nebo naopak, z ceny s DPH vypočítat cenu bez DPH. Myslíte si, že to je jasná záležitost? Čtěte dál.

Desetinná čísla

Pokud jste četli můj předchozí článek o PHP pro začátečníky, zmiňuji se zde o typu (float) což je v php typ reprezentující desetinná čísla.
Nutno dodat, že v PHP je toto jediný typ pro desetinná čísla. Podle manuálu existují i typy (double) a (real), ve skutečnosti jde o aliasy typu (float). Velikost floatu je variabilní v závislosti na procesoru, na kterém se PHP nachází.

Počítače vše počítají v binární soustavě - nuly a jedničky. Číslo 10 je v desítkové soustavě číslo 2, 110110 je číslo 54. To je všechno ok, ale i binární soustava počítá s desetinnými čísly.
Kolik je v desítkové soustavě 10,1? Výsledek: 2,5.
10,011 = 2,375.
Ale co naopak? Kolik je v binární soustavě číslo 2,6 ?
Výsledek: 10.10011001100110011001100110011001....a do nekonečna opakující se 1001 v desetinné části. Počítač ale nemůže iracionální nebo periodická čísla reprezentovat nekonečně přesně, procesor nemá nekonečně širokou sběrnici, takže v nějaké části číslo ořízne a 2,6 nebude 2,6 ale stane se z něj 2,5999999046325684.
Počítač navíc neukládá desetinné číslo přesně v tom formátu, v jakém jsem ho napsal ale ve formátu IEEE 754, který mu umožňuje jednoduššeji provádět výpočetní operace.
V počítači tedy není možné reprezentovat desetinné číslo s maximální přesností, neboť i pro binární soustavu mohou po převodu vznikat čísla s neukončeným periodickým rozvojem v desetinné části a musí se někde odstřihnout, čímž dojde ke ztrátě přesnosti.
Teď vás nejspíš napadá argument: vždyť já nepotřebuji dokonalou přesnost, stačí mi čísla zaokrouhlovat na dvě desetinná místa, halíře se nepočítají na tisíciny. A navíc, dneska má skoro každý 64 bitový procesor což dává prostor pro ještě přesnější čísla.
No jo, jenže když zaokrouhlíte čísla, jejichž skutečná reprezentace není přesně to číslo, které vidíte na obrazovce, vyjde vám to blbě. Mějme následující učebnicový příklad:

<?php 
$p 
= (0.7+0.1) * 10;
echo 
floor($p);
?>
Víte, co je výsledkem? Ne, nikoliv 8 ale 7. Proč?
0,7 je ve dvojkové soustavě nekonečně dlouhé číslo - 0.10110011001100110011... s nekonečně se opakujícími bity 0011.
0,1 je ve dvojkové soustavě to samé jen s jiným začátkem - 0.0001100110011...
Obě dvě čísla tedy nemohou být reprezentována s definitivní přesností. Výsledek výrazu (0.7+0.1) * 10 je sice 8, nicméně vnitřní reprezentace výsledku bude 7,99999... a přetypování na (int) nebo použití zaokrouhlení floor desetinnou část správně odřízne.

Co z výše uvedeného vyplývá?
Na (float) se nemá cenu spoléhat. Jde o peníze a potřebujeme se na veškeré výpočetní operace spolehnout a nesmíme se minout ani o haléř.
V eshopu se nemusí výpočet ceny týkat jen daně, můžete naimplementovat třeba modul který jednotlivé produkty navýší o procentní marži nebo naopak sníží o nějaké slevy, akce, kupony, smluvní dohody o cenách od jednotlivých dodavatelů a bůhví, co všechno si takový čiperný podnikatel může vymyslet.
Verdikt? Pro účely počítání cen NIKDY NEPOUŽÍVEJTE FLOAT.

BCMath

V PHP je knihovna BCMath, jejíž používání neobsahuje rizika spojená s používáním typu float. Tato desetinná čísla se totiž používají jako řetězce a funkce této knihovny jsou matematické operace. BCMath se stará o přesnost na počet desetinných míst, které si sami zvolíte, třeba na 100, to je jedno, přesnost máte vždy zaručenou protože BCMath nepracuje s floaty ale řetězci, které sám propočítává celočíselnou aritmetikou a s floatem nemá nic společného.
Jednotlivé funkce BCMath ani nepotřebují abych je nějak rozebíral, jsou to funkce, kde první operátor je levá strana rovnice a druhý operátor je pravá strana rovnice. Jednotlivé funkce reprezentují pak matematickou operaci a výsledkem je buď string (výsledek matematické operace) nebo boolean (výsledek porovnání), viz. zde.

<?php 
$c 
bcadd ($a$b); //$c = $a + $b;
$c bcsub ($a$b); //$c = $a - $b;
$c bcmul ($a$b); //$c = $a * $b;
$c bcdiv ($a$b); //$c = $a / $b;
$c bcpow ($a$b); //$c = $a ^ $b;
$c bcsqrt($a);     //$c = sqrt($a);
?>

Zásadní funkce je bcscale - touto funkcí zadáte, na kolik desetinných míst mají funkce BCMath počítat. Každá z jednotlivých matematických funkcí má pak třetí volitelný parametr, kam můžete zadat počet desetinných míst jen pro tuto operaci...mnohem pohodlnější však je nastavit nějakou výchozí hranici, do které chceme znát desetinná čísla a z té vycházet - nicméně i tento třetí parametr budeme potřebovat, kvůli zaokrouhlování.

Zobrazování cen

Ještě než začneme kapitolu o tom, jak se ceny počítají tak je nutné si nejdřív říct, jaké ceny chceme zobrazovat.
První věc, kterou je nutné brát v potaz: BCMath nezaokrouhluje. Pokud u BCMath nastavíte přesnost na dvě desetinná místa, z čísla 5,749999 se stane 5,74 (další desetinná místa se zahodí). Na zaokrouhlování nemůžeme využít funkce floor, ceil nebo round, neboť tyto funkce pracují jen s floaty, nikoliv s řetězci.
Budeme muset napsat vlastní funkce ale to nebude tak těžké, jak si myslíte.
A pokud nejsme úplně blbí, vygooglime hotový kód ze StackOverflow :D

<?php 
//bcceil - zaokrouhlení na celá čísla nahoru
function bcceil($number) { 
    if (
$number[0] != '-') {        
        return 
bcadd($number10);
    }
    
    return 
bcsub($number00);
}
//bcfloor - zaokrouhlení na celá čísla dolů 
function bcfloor($number) {
    if (
$number[0] != '-') {
        return 
bcadd($number00);
    }
    return 
bcsub($number10);
}
//bcround - matematické zaokrouhlení na $precision desetinných míst
function bcround($number$precision 0) {
    if (
$number[0] != '-') {
        return 
bcadd($number'0.'.str_repeat('0'$precision).'5'$precision);
    }
    return 
bcsub($number'0.'.str_repeat('0'$precision).'5'$precision);
}
?>

Jednotlivé funkce pro zaokrouhlování čísla využívají jen úplně obyčejně jednoduché a funkční fígle s matematikou a řetězci. Zaokrouhlování využívá třetího parametru, kde vložím nulu (pokud zaokrouhluji na celé číslo čímž řeknu, že vrácené číslo neobsahuje desetinnou část) nebo parametr (pokud zaokrouhluji na konkrétní desetinné místo pomocí bcround).
(Jinak řečeno: na vysvětlování toho jak v tom ta matematika a řetězce fungují jsem moc línej)

Jak lze vlastně cenu zaokrouhlit? Není zaokrouhlování nějak určeno zákonem?
Je a není - zákon se zmiňuje o zaokrouhlování jen při výpočtu daně ale co se týče finálního zaokrouhlení ceny, obchodník jí může zaokrouhlit jak chce - na haléře, desetihaléře a padesátihaléře nebo celé koruny.
Z toho vyplívá, že obchodník si sám může zvolit, jakou zaokrouhlovací metodu zvolí. Může chtít zaokrouhlovat vše dolů, vše nahoru, matematickým zaokrouhlením, postupným zaokrouhlením... (jedná se však o stanovení ceny, při vyplňování daňových formulářů jsou zákony ohledně zaokrouhlování o něco striktnější)
Takže si domyslíme, že v kódu potřebujeme počítat nejméně na 3 desetinná místa, neboť pro zaokrouhlování na nejmenší povolenou jednotku - na haléře - je třeba třetí desetinné místo.
Příklad: Máme vypočtenou cenu 15.544, obchodník má tři volby:
15,54 - při zaokrouhlování dolů
15,55 - při zaokrouhlování nahoru
15,54 - při matematickém zaokrouhlování na dvě desetinná místa

Mnojo, jenže s tím, že obchodník může zaokrouhlovat jak chce je trochu problém. Co když vám řekne, že veškeré výsledky chce třeba postupným zaokrouhlováním z 8 desetinných míst? No, odpověď je jednoduchá. Budete mu muset takovou funkci napsat :D Na druhou stranu, nemělo by to být tak těžké, bcscale nastavíte na 8 a tím pádem se všechny ceny budou počítat na 8 desetinných míst a pak napíšete funkci, kde odtrhnete desetinou část z řetězce a budete ho v cyklu procházet od konce do druhého desetinného místa, každé desetinné místo zaokrouhlíte a podle výsledku do další iterace přenesete prázdnou hodnotu nebo navýšení. Navíc, jakmile jednou tuto funkci napíšete, budete jí moci využít pro každého dalšího šíleného obchodníka.

Počty v příkladech

Řekněme, že máme obchodníka č.1, hodně tvrdohlavého obchodníka, který chce eshop bez desetinných míst úplně.

Obchodník č. 1: Nechci vůbec na eshopu desetinná místa, nastavíme bcscale(0) a veškeré výpočty se budou zaokrouhlovat prostým matematickým pravidlem.
Obchodníkův argument: zákon 634/1992 Sb. §3 odst. c) nám říká: při konečném účtování prodávaných výrobků a poskytovaných služeb v hotovosti se celková částka zaokrouhluje vždy k nejbližší platné nominální hodnotě zákonných peněz v oběhu.
Protiargument: To neznamená, že každý obchodník za každou cenu musí všechny své ceny zaokrouhlit! Zákon nám jen říká, jak zaokrouhlit platbu v hotovosti ale v bezhotovostním styku desetinná místa stále fungují. Je v režii banky, jak se s desetinnými částkami vypořádá. Pokud chcete v eshopu mít Paypal nebo platební systém České Spořitelny, je nutné s tím počítat!
Definitivní protiargument: Dále je třeba počítat s €ury, které se časem v ČR objeví. A v eurech se platí centy úplně běžně, bylo by zatraceně krátkozraké předpokládat, že moje ceny budou vždy celým číslem. Proč? Při zavedení eura se dá čekat, že vláda vydá zákon, kterým všem obchodníkům přikáže do jaké míry smějí ceny zaokrouhlovat, jinak by se zde vše pochopitelně začalo zdražovat což by do nějaké míry hnulo s ekonomikou celého republiky.
Někteří obchodníci mají velmi striktní cenovou politiku, do které nechtějí zasahovat (příkladem Baťa, a jeho ceny nikdy nekončící na nic jiného než na devítky). Od takových obchodníků/manažerů je třeba zjistit, jestli s přechodem na Euro počítají. Pokud nebudete počítat s desetinnými místy a ČR přejde na Eura, budete muset eshop překopat a pokud nebudete mít část pro výpočet zobrazované ceny v kódu centralizovanou třeba v jedné třídě, vyhodí vás z práce, protože to nestihnete a klient nezaplatí.

DPH a základní cena

Představme si obchodníka č. 2.

Obchodník č.2: Veškerý můj sortiment má stanovené ceny s DPH, nikdy nemám cenu bez DPH. Chci mít možnost vkládat do administrace ceny S DPH a na frontendu chci zobrazovat obě ceny: s DPH i bez DPH. Chci, aby klient při potvrzování objednávky viděl, kolik činí samotné DPH u jednotlivých produktů. Částku zaokrouhluji na haléře nahoru.
S Eury počítám, po přechodu na Euro naimportuju jiný ceník.
Rozbor: Potřebujete z konečných cen s DPH vypočítat částku bez DPH a DPH samotné.
Možná si říkáte: vypočítat DPH je logické, pro produkt s cenou 299 a daní 20% vypočtu přímo cenu bez DPH 299 / 1,20 a výsledek je 249,166666...
Omyl!
V rozboru jsme udělali hned dvě chyby.
Za prvé - mějte na vědomí, že při dělení nám může vyjít číslo s neukončeným desetinným periodickým rozvojem...prostě s donekonečna se opakujícími čísly, my však počítáme jen na 3 desetinná místa. Musí nám vycházet rovnost a pokud u jiného obchodníka zvolíme zaokrouhlování dolů, nebude nám tímto postupem nic vycházet. 299 / 1,20 = 249,16 ale naopak to nevyjde - 249,16 * 1,20 = 298,99! Tento postup je špatný už jen kvůli tomu dělení, v obecné rovině počítání cen se chceme zbavit všech "desetinně neukončených" čísel.
Za druhé - pokud se zjistí, že obchodník byl vrah, lupič, podvodník co okradl stát na daních o miliony a uteče na Bahamy, policie si prohlídne vše, co tu nechal, mimo jiné i jeho eshop. Z jeho zdrojáku zjistí, že jste špatně počítali daň, protože CENA BEZ DPH * 1,20 nebo CENA S DPH / 1,20 není zákonem stanovený postup pro výpočet DPH.
Rozumím vám, říkáte si, WTF? Vždyť jak jinak spočítat daň než normální matematickou operací?
DPH se musí počítat zákonem stanoveným způsobem. Jsou dva odstavce, jeden říká, jak počítat daň pro cenu s dph a druhý který nám říká, jak z ceny, kde je DPH už zahrnuto spočítat právě tuto daň respektive cenu bez DPH.
Přesné znění je v zákoně o dani z přidané hodnoty č. 235/2004 Sb. §37, odstavec 1) a 2)
(1) Daň se vypočte ze základu daně stanoveného podle § 36 jako součin úplaty za zdanitelné plnění bez daně a koeficientu, který se vypočítá jako podíl, v jehož čitateli je číslo 20 v případě základní sazby daně nebo číslo 10 v případě snížené sazby daně a ve jmenovateli číslo 100, vypočtený koeficient se zaokrouhlí na čtyři desetinná čísla, vypočtená daň se uvede způsobem podle § 28 odst. 2 písm. l). Cena včetně daně se pro účely tohoto zákona dopočte jako součet základu daně a vypočtené daně po případném zaokrouhlení.
(2) Daň může plátce rovněž vypočítat z úplaty za zdanitelné plnění, která je včetně daně, nebo z částky stanovené podle § 36 odst. 6, která je včetně daně, a koeficientu, který se vypočítá jako podíl, v jehož čitateli je číslo 20 v případě základní sazby daně nebo číslo 10 v případě snížené sazby daně a ve jmenovateli součet údaje v čitateli a čísla 100, vypočtený koeficient se zaokrouhlí na čtyři desetinná místa, vypočtená daň se uvede způsobem podle § 28 odst. 2 písm. l). Cena bez daně se pro účely tohoto zákona dopočte jako rozdíl částky za zdanitelné plnění obsahující daň a vypočtené daně po případném zaokrouhlení.

A já si myslim, že jste ty odstavce se zákony vůbec nečetli protože se to čte blbě a je to nuda. Chápu vás.

Obchodník č.2 používá jen ceny s dph ale i přesto náš eshop musí umět vypočítávat cenu BEZ dph do kalkulací v administraci. Zákon nám říká, že se DPH vypočítá násobením částky koeficientem-zlomkem, v jehož čitateli je výše daně a ve jmenovateli 100 + výše daně, výsledek je zaokrouhlený na 4 desetinná místa.
V matematice platí, že 20 / 120 * cena = cena - (cena / 1,2) jenže zákon explicitně říká, že výsledek (20 / 120) je třeba ještě zaokrouhlit na 4 desetinná místa
Pokud jste matematicky myslící tvor, pak jste v tuhle chvíli celý princip pochopili.
Pokud nejste matematicky myslící tvor (jako já), pak mám pro vás následující tabulku, která vám to osvětlí.


Výsledky jsou podle požadavků obchodníka vždy zaokrouhleny na 2 desetinná místa nahoru.
Cena S DPH Matematický výpočet:

daň = cena - (cena / 1,2)
Zákonem stanovený výpočet:

daň = cena * 0,1667
O kolik je částka stanovená zákonem vyšší než matematicky vypočtená částka - neboli o kolik bychom mohli eventuelně stát okrást
5,- 0.83 0.83 0.00
10,- 1.67 1.67 0.00
19,- 3.17 3.17 0.00
48,- 8.00 8.00 0.00
299,- 49.83 49.84 0.01
1999,- 333.17 333.23 0.06
5999,- 999.83 1000.03 0.20
10000,- 1666.67 1667.00 0.33
59999,- 9999.83 10001.83 2.00
100000,- 16666.67 16670.00 3.33
599999,- 99999.83 100019.83 20.00
1500000,- 250000.00 250050.00 50.00
29999990,- 4999998.33 5000998.33 1000.00

Teď je to jasné všem - zákonem stanovená metoda totiž saje do státní pokladny o něco víc peněz, než matematická metoda. Možná si ťukáte na čelo a říkáte si, že v eshopu vaší firmy se přeci nikdy nebudou prodávat částky za desetitisíce, statisíce a miliony a koho zajímají nějaké haléře...
Obchodníka to zajímá. Zákazníky to zajímá! Všechny to zajímá! Je zatraceně hloupé si myslet, že je přeci jedno když nedodržujeme zákony byť v nich jde jen o haléře. Ano, máte pravdu pokud mi řeknete, že to nejspíš ani většina maloobchodníků vašeho malého eshopu neví. A chcete vědět, jak zareagují, až se to dozví? Líbit se jim to nebude.

Panikaříte, protože jste tohle nevěděli a už těch eshopu jste prodali moc?
Nemusíte, věřte mi, že eshopy, kde se cena počítá přes float za pomoci primitivní matematiky bez znalosti zákonů prodává nejspíš většina firem. Prošel jsem přes pár zaměstnavatelů, žádný z nich o zákonem stanoveném způsobu výpočtu DPH nevěděl a žádný z klientů (kterých bylo pochopitelně mnohem víc) se na to ani nikdy neptal.

První odstavec říká, že se DPH z ceny BEZ DPH vypočte vynásobením ceny číslem (daň / 100) zaokrouhleným na 4 desetinná místa a celková cena je pak cena bez dph + daň...
tj. 20 / 100 = 0,2000 ...tedy 0,2 a to je LOL, že jo, protože tenhle postup dá jiné číslo, než postup opačný.
daň z 1.000.000 bez daně = 200.000 Kč
ale
daň z 1.200.000 s daní     = 200.040 Kč
Tohle nedává smysl, že?
Vtip je totiž v tom systému, jakým DPH funguje. V řetězci mezi spotřebitelem a dodavateli nemáte vždycky k dispozici cenu bez DPH, navíc, někdo je plátcem a někdo není plátcem. To už bych ale příliš odbočoval k daním.

Shrnutí

Jak ukládat cenu?
Vycházím z toho, že programujete úplně obyčejný eshop a nenapadá vás žádný ideální způsob. Pokud součástí vašeho eshopu je dynamická aktualizace cen podle nějakých analýz, pak nejspíš sami dobře víte, jak s takovýma cenama pracovat.
Cenu jako takovou ukládejte do databáze (pochopitelně k tabulce která se týká dané položky) jako desetinné nezáporné číslo minimálně na 3 desetinná místa. Vždy vycházejte z toho, že jde o cenu BEZ DPH, které eshop vypočte, pokud obchodník do administrace zadá cenu včetně DPH. Takto s cenou zacházejte i pokud klient používající váš eshop vlastně ani není plátce a DPH na svém eshopu nechce. Jednou se z něj plátce třeba stane a pak je mnohem jednodušší někde jen přepnout nějaký boolean než překopávat celý eshop.
Číslo potřebujete zaokrouhlené nejméně na 3 desetinná místa, neboť v bezhotovnostním styku (PayPal, platební brána ČS, bankovní příkaz) je možné částku zaplatit i s haléři...a aby jste měli možnost zaokrouhlit na haléře, musíte mít k dispozici číslo ještě za tím haléřem.

Jak zobrazovat a zaokrouhlovat cenu?
Tak jak chtějí obchodníci - a každý obchodník to chce jinak a nikdy to nechce všude stejně. Takže třeba v košíku chce zobrazovat vše s desetinnými čísly a výsledek zaokrouhlit na celé koruny, některý chce zase v košíku vše zaokrouhlovat na celé koruny a zobrazovat na zbytku eshopu ceny na jedno desetinné místo, někdo jiný zase chce zobrazovat částky na dvě desetinná místa jen cenu bez DPH nebo s DPH nebo se slevou...
A co ceny na backendu? Jaké ceny zobrazit v objednávkách, jaké na fakturách? Možností je hrozně moc!

Jak ukládat DPH?
Stejným způsobem, jakým je DPH reprezentována ve skutečnosti - avšak ukládejte jí jako desetinné číslo. Že je teď základní sazba 20% neznamená, že jí vláda nesníží jen o 0,03 procenta, jakkoliv úchylně by sazba 19,97% vypadala.

Jak to celé nějak naprogramovat?
Motigovo pravidlo pro začátečníky:

NIKDY NEPIŠTE JEDEN VÝPOČET DVAKRÁT!

Jednoduše řečeno: centralizujte kód týkající se výpočtu ceny do jedné třídy nebo funkce. Na celém eshopu, na frontendu i na backendu je miliarda míst, kde se cena zobrazuje a pokaždé jí zde potřebujete zobrazit jinak. Cena, kterou zobrazíte, MUSÍ vycházet z jednoho místa, nejlépe ze třídy.
Konfigurace cen by dle mého úsudku neměla být editovatelná z administrace ale z konfigurace, kterou má k dispozici jen programátor. Pravda, na tohle už můžete mít jiný názor. Mám pro to ale dva důvody: první důvod je ten, že způsob zobrazení a výpočtu ceny je pro obchodníka zásadní a jakmile jednou řekne, jak chce ceny zobrazovat a zaokrouhlovat, už nebude chtít to měnit. V druhém důvodu vycházím z toho, že zákazníci nebo váš šéf chce do eshopu víc, než jen DPH...jak jsem se zmínil na začátku, cena může zahrnovat cenovou akci, marže a podle marží sledovat skutečné zisky, slevy atd. apod. Všechny tyhle věci ovlivňují cenu a je HROZNĚ ŠPATNĚ, pokud tyto věci naprogramujete mimo centralizovanou část ceny!

Příklad

Na frontendu na hlavní stránce zobrazuji 5 řádků po 4 sloupcích, celkem 20 položek. První řádek obsahuje jen zboží v akci seřazeno podle data přidání, další zboží je vybrané zcela náhodně.
Špatné řešení databáze: Akční cenu mám uloženou jako sloupec k tabulce s položkami. Pokud je obsah sloupce NULL nebo 0, zboží v akci není, jinak je akční cena právě ta, která je v tomto sloupci.
Správná řešení databáze: Akční ceny mám uložené ve vlastní tabulce nebo v jiném relačním schématu.
Špatná implementace: Provedu dva dotazy: jeden dotaz, kde JOINuji sloupec s akčními cenami, druhý dotaz, kde vypisuji normální ceny. Pak data procházím normálně v cyklech, v jednom vypíšu sloupec s akční cenou, v druhém cyklu vypíšu sloupec s normální cenou.
Správná implementace: Objektová implementace - jednotlivé zboží je reprezentováno objekty jejichž ceny jsou též reprezentovány objekty. Mám stále dva dotazy nebo jiný, nacachovaný zdroj dat, ve kterém mám vše z databáze předem vyndané. Pokud chci zásadně dodržovat OOP abstrakci tak mám filtrační metody, kterými vyberu jednou nejdřív akční zboží a pak náhodně seřazené ostatní zboží, nebo na menší úrovni abstrakce zavolám nějaké dva dotazy, které oba berou stejné sloupce a stejné joiny, jenom mají jiný "WHERE", výsledky dotazů jsou pak zdrojem dat pro konstrukci objektů. Při výpisu ceny si pak z objektu ceny mohu vybrat, jakou cenu chci zobrazit.

Příklad č. 2

Cena se skládá z vlastní marže, dodavatelské marže a pokud je zákazník na eshopu přihlášený tak je cena ještě upravena o částku dočasného slevového kuponu, kterou zákazník může vložit prostřednictvím nastavení svého účtu na frontendu.

VELMI špatná implementace: Všude kde se zobrazuje cena beru ještě data z tabulek vlastních marží, dodavatelských marží a z tabulky kupónů klientů. Před zobrazením ceny částku vždy spočítám a pak zobrazím.
Pořád špatná implementace: Všude kde se zobrazuje cena beru ještě data z tabulek vlastních marží, dodavatelských marží a z tabulky kupónů klientů. Počítání už nedělám, protože na to jsem si napsal nějakou funkci, do které dám všechny parametry, které potřebuje a vrátí mi správně spočítanou cenu.
Lepší, ale pořád špatná implementace: Všude kde se zobrazuje cena beru ještě data z tabulek vlastních marží, dodavatelských marží a z tabulky kupónů klientů. Počítání už nedělám, protože na to jsem si napsal nějakou funkci, do které dám ARRAY parametrů, tudíž mohu funkci v budoucnu rozšířit o jakékoliv další možné propočty s funkcí aniž bych furt jak blb psal funkci s miliardou parametrů na několik řádků.
Správná implementace: OOP: jednotlivé zboží je reprezentováno objekty jejichž ceny jsou též reprezentovány objekty. V závislosti na tom jaké zboží chci zobrazit buď zavolám dotaz se sloupci, které objekt potřebuje pro správnou funkčnost nebo mám jiný, nacachovaný zdroj dat, ve kterém mám vše z databáze předem vyndané. Při výpisu ceny pak vytáhnu již hotovou vlastnost z objektu ceny ke které se dostanu např. přes $seznam_zbozi_hlavni_stranka->zbozi[0]->cena->finalni_cena
nebo
$seznam_zbozi_hlavni_stranka->getZbozi(0)->getCena()->getFinalniCena()
Sejde na tom, jak se vám to líbí víc.

Špatná implementace objektu ceny:
Objekt ceny mi sám zjistí všechny potřebné údaje z databáze.
Správná implementace objektu ceny:
Všechny údaje o maržích, dodavatelských maržích a kuponech mám buď nacachované (např. ve statických vlastnostech objektu ceny) nebo všechny JEDNOU vybrané z databáze tak, abych nemusel posílat miliardu dotazů na databázi jak magor pro zjištění ceny každé položky, kterou chci zobrazit.
Uvědomte si také, že při počítání ceny hrozně záleží na pořadí. Tady už ale záleží na implementaci databáze, správný přístup však je všechny tabulky ovlivňující cenu cachovat a veškerá data mít kdykoliv k dispozici.

Nastavovat nebo nenastavovat...

Jak už jsem jednou psal, nestalo se mi, aby obchodník najednou změnil svůj požadavek v tom jak zobrazovat cenu (což pochopitelně neznamená, že se to nemůže stát nikdy). Ono to i dává smysl - pro obchodníky je moc důležité, jakou cenu nabízejí a jak ta cena vypadá v eshopu, v objednávce a nebo na faktuře. Tohle mě spíš vede k tomu mít možnost nastavit zobrazení ceny a zaokrouhlování přes nějakou programátorskou administraci, interní XML nebo INI soubor nebo konfiguraci z databáze dosažitelnou jen z phpmyadminu (záleží na vaší implementaci).

Tabulka toho, co by váš eshop měl umět nastavit:

Zaokrouhlovat na:
  • haléře
  • desetihaléře
  • padesátihaléře
    o trochu těžší implementace, přemýšlejte&implementujte...
    ...nebo googlujte:D
  • celé koruny
Zaokrouhlovací metoda:
  • dolů
  • nahoru
  • matematicky ((int)x < 5 dolů, (int)x > 4 nahoru)
Zobrazit zaokrouhlení (běžné na fakturách):
  • ano
  • ne
Kam nastavení aplikovat:
  • náhled zboží
  • detail zboží
  • proces objednání zboží z košíku
  • faktury
  • [...kdekoliv jinde chcete]

Objekt ceny

Tady už ode mně máte jen doporučení co se týká samotného kódu.
Protože se cena dá ovlivnit nekonečně mnoha způsoby, definuji termín "finální cena". Finální cena je ta cena, která se právě zobrazuje. Finální cena NENÍ cena produktu, kterou klient skutečně zaplatí, protože pokud váš eshop umožňuje při vyřizování objednávky vložit slevový kupon na konkrétní zboží, finální cena se upraví.
Já osobně mám rád přístup k vlastnostem přes __get a u ceny zrovna tak. Vzhledem k různorodosti požadavků obchodníků přistupuji k vlastnostem ceny pomocí metody __get, která mi zjistí, co vlastně chci a podle toho obvolá privátní metody třídy.

<?php 
$zbozi
->cena->finalni_cena
//obsahuje hlavní, finální cenu, kterou klient zaplatí.
//tato cena je zobrazena a zaokrouhlena podle nastavení eshopu

$zbozi->cena->finalni_cena_ceil
//obsahuje hlavní, finální cenu, kterou klient zaplatí,
//zaokrouhlenou na celá čísla pomocí bcceil

$zbozi->cena->finalni_cena_round2
//obsahuje hlavní, finální cenu, kterou klient zaplatí,
//zaokrouhlenou na haléře (číslo za "round" je použito jako druhý parametr
//pro funkci bcround)

$zbozi->cena->finalni_cena_bez_dph
//Obsahuje hlavní, finální cenu, bez DPH. 
//Tato vlastnost nemusí existovat, pokud obchodník není plátcem 
//a DPH nikde nezobrazuje. Závisí na vaší implementaci, jestli půjdete
//striktní cestou exceptionů, nebo budete vracet prázdnou hodnotu
$zbozi->cena->finalni_cena_bez_dph_ceil
$zbozi
->cena->finalni_cena_bez_dph_round2

$zbozi
->cena->akcni_cena_rozdil
$zbozi
->cena->akcni_cena_rozdil_ceil
$zbozi
->cena->akcni_cena_rozdil_round2
//UŠETŘÍTE AŽ X !!!, kde X je hodnota této proměnné...
//může být v procentech či číslech a existuje jen u zboží, 
//které je zrovna v akci. 
?>
V metodě __get v objektu ceny tedy rozlišuju, co je na konci názvu požadované proměnné. V případě _ceil vracím celé číslo, v případě _roundX vracím číslo zaokrouhlení na X desetinných míst. Můžete si to ale udělat jak chcete, třeba _zaokrouhlitDesetniky, _zaokrouhlitPadesatniky, _zaokrouhlitKoruny...

Budu vděčný za jakékoliv připomínky a komentáře! x)


<< Zpět ...
Endeer | q@endeer.cz
Jakože dělat for cyklus s floatem se ale snad ví, že je špatný nápad ;) Třeba i jen kvůli korektnímu počtu proběhnutí cyklu. Ale když bys udělal for($i = 1; $i <= 10; $i = round($i + 0.01, 2)) { echo $i; } tak by to mělo fungovat korektně a to i včetně zastavení právě po desítce, ne třeba kvůli nepřesnostem před/po ní. Ale i tak bych to asi neudělal a iteroval radši 100-1000 a dělil to stovkou až při použití v cyklu.

To ale neporušuje to co jsem řekl - ať už s tou cenou děláš cokoliv (rozumného), ta chyba se nemůže projevit dost vysoko, aby to způsobilo chybu byť v haléři. Tedy dokud se používá double a ne float. Zdá se že některé kompilace PHP možná používají float, který přecejen moc míst neumí a mohlo by to vadit, nicméně to je pak asi potřeba změnit hosting. Naštěstí v tomto ohledu se nemusím podřizovat tomu, že to musí fungovat na každém (žel v PHP to být muselo...).

Ad js text - omlouvám se že to znělo trochu jako "každý ví". Pouze to je asi nejrozumnější řešení, na které jsem natrefil, jednoduché na udělání a JS aspoň zatím spamboti neumí.
Uživatelský avatarMotig | Web | motiggolddragon@gmail.com | 257433009
Eender: v článku jsou funkce pro zaokrouhlení pomocí bc funkcí (s funkčností totožnou s round).
Zopakuju to co jsem psal v předchozím komentáři a v samotném článku.
"Kód roste, přibývá možností manipulace s cenou a tím pádem přibývá i operací s floaty."
round ti nijak nepomůže, pokud cena před konečným zobrazením proběhne několika na sobě nezávislými operacemi, nedejbože na několika na sobě nezávislých místech v kódu. Myslim, že na tohle ještě doplnim do článku nějaký příklad.

Stala se mi dokonce s floatem věc, že při procházení for cyklu po floatu od 1.00 do 10.00 po 0.01 se proměnlivě při refreshování sem tam vyechovalo číslo typu x.yz9999999 nebo x.yz00000001 O.o (v PHP 5.3 na pracovním serveru). Na vlastním PC ani noutebooku (win7, winXP) se mi to nikdy nestalo ale i tak, jde o peníze a po těhle zkušenostech nemám ve float víru.

btw dík za tip s JS textem
Endeer | f@endeer.cz
Na co jsem narazil vtipnějšího bylo MySQL 4.1 (resp. dle manuálu až do verze 5.0.3) a jeho implementace funkce ROUND() - totiž na linuxu standardní implementace C funkce round() vrátí 1,5 => 2, ale 0,5 => 0. Rozuměj zaokrouhluje k bližšímu sudému číslu, že je to tak prý statisticky lepší či že co. No ale když tohle udělá eshop...

(mluvím o použití datatypu decimal; u floatu to dělá dodnes - teda na linuxu - na Woknech systémová funkce zaokrouhluje floaty normálně).
Endeer | f@endeer.cz
U floatu chápu velmi dobře oč jde, ale přijde mi, že se používáním bc funkcí problém neodstraní. Stále totiž např. před uložením do databáze je nutné provést manuální zaokrouhlení, neboť bc jen ořezává. A když použiju float a před uložením udělám round($x, 2), tak to vyjde vždy správně, ať počítám, co počítám - ujede možná n-té desetinné místo, ale to mne netrápí a funkcí round() zajistím, že se nevypíše ani žádný pokus o 0.79999999999.

Věřím že v bankovním systému to je jiné, tam už můžou lítat desítky-stovky milionů a chyba by se mohla zpropagovat dostatečně vysoko, aby nějakým tím haléřem hnula, nicméně nevěřím že existuje eshop, ve kterém by se problém s floaty (resp. doubly) projevil. Tudíž bc považuju za zbytečné zesložitění.

A co se antispamu týče, standardní řešení bývá "opište text", který se předvyplní a skryje javascriptem, je-li přítomen, ale umožní vyplnění když JS není. Ve své podstatě mi ale vadí pouze, že mi to ten text sežere - nedozvím se chybu a přijdu o text, pokud by mi ho prohlížeč neuchoval v paměti po stisknutí [zpět].
Uživatelský avatarMotig | Web | motiggolddragon@gmail.com | 257433009
Eender: Tohle má být článek pro začátečníky a rozhodně neslouží jako pravidlo. Cachovat ceny je už další vrstva implementace.

S floatem nevím, jestli jsi správně pochopil, co se v něm snažím vysvětlit. Je docela obtížný (ale ne nemožný) hlídat správný zápis a výpočty, pokud se rozhodneš používat float. A pokud se někde sekneš třeba jen o halíř při blbě položeném zaokrouhlení nebo přetypování, je to problém (což se týká toho výpočtu (0,1+0,7)*10 který je mimojiné z oficiálního manuálu na php.net). Vyhnout se floatu za každou cenu je to samé, jako vyhnout se používání register_globals...používat to můžeš ale musíš dobře vědět, co děláš. A kód roste, přibývá možností manipulace s cenou a tím pádem přibývá i operací s floaty...
...
S vypnutým JS mi tady napsat komentář nepůjde nikdy, varianty typu "antispam kontrola: odpověz na otázku" nebo captcha nemám rád. A zatim jsem byl línej vložit nějaký upozornění do <noscript>u x)
Endeer | f@endeer.cz
(rád bych podotkl, že se mi nelíbí, že mi to bez zapnutého javascriptu spolkne komentář a nezávisle na tom bych ještě rád upřesnil, že eshop jsme psali před tímto čánkem, ne po něm)
Endeer | f@endeer.cz
Tak hlásím, že jsme psali eshop, který o zpětném určování ceny bez daně pomocí násobení číslem zaokrouhleným na čtyři desetinná ví ;)

Nicméně co se výpočtu cen týče, vše se při ukládání produktů/kategorií předpočítává pro všechny možnosti do cache tabulky cen, neboť netuším jak bych jinak při pár tisících produktech udělal byť jen jednoduchý požadavek návštěvníka eshopu "max. cena = x" a nesložil přitom server.

Floatu bych se tak nebránil. Jen musím vědět, že je maličko nepřesný (byť tedy ne tak strašně jak píšeš ve článku, (0.1+0.7)*10 mi vyšlo 7.99999999999999 a až teprve pak nějaký bordel) a pomoct mu do správné podoby když se ukládá/zobrazuje.
Flaiming | Web | flaiming@centrum.cz
Tyjo, ty jsi si s tim fakt vyhral :) Az budu delat eshop, tak se na tohle urcite podivam.

Jinaki oprav si tam drobnou chybku, mas tam nekde "vyplívá" ;)
Jméno:
Email:
Web:
ICQ:
Vzkaz:
Jméno:
Heslo:
Registrovat
Posílat oznámení o novinkách
obalka Vlož svůj email
 
rssimg RSS Odběr
Motig © 2008 - 2012
forum powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group
phpBB AcidTech Red skin by STSoftware
Mapa stránek
Česky English