From 12f47b48ba88590cb219123cd7b83afebacdeb2b Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer <reinhold@kainhofer.com> Date: Thu, 14 Feb 2013 14:21:37 +0100 Subject: [PATCH] Version 2.0: Implement OR, coupon variables, prices w/o tax, better parsing, etc. --- Makefile | 6 +- .../en-GB.plg_vmshipment_rules_shipping.ini | 4 +- ...mshipment_rules_shipping_advanced_v2.0.zip | Bin 0 -> 17832 bytes .../plg_vmshipment_rules_shipping_v2.0.zip | Bin 0 -> 14616 bytes rules_shipping.php | 609 +-------------- rules_shipping.xml | 5 +- rules_shipping_advanced.php | 215 ++++-- rules_shipping_advanced.xml | 6 +- rules_shipping_base.php | 694 ++++++++++++++++++ 9 files changed, 863 insertions(+), 676 deletions(-) create mode 100644 releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip create mode 100644 releases/plg_vmshipment_rules_shipping_v2.0.zip create mode 100644 rules_shipping_base.php diff --git a/Makefile b/Makefile index cbbb226..37b7df0 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ BASE=rules_shipping BASE_ADV=rules_shipping_advanced PLUGINTYPE=vmshipment -VERSION=1.1.0 +VERSION=2.0 -PLUGINFILES=$(BASE).php $(BASE).script.php $(BASE).xml index.html -PLUGINFILES_ADV=$(BASE_ADV).php $(BASE).php $(BASE_ADV).script.php $(BASE_ADV).xml index.html +PLUGINFILES=$(BASE).php $(BASE)_base.php $(BASE).script.php $(BASE).xml index.html +PLUGINFILES_ADV=$(BASE_ADV).php $(BASE)_base.php $(BASE_ADV).script.php $(BASE_ADV).xml index.html TRANSLATIONS=$(call wildcard,language/*/*.plg_$(PLUGINTYPE)_$(BASE).*ini) TRANSLATIONS_ADV=$(subst $(BASE),$(BASE_ADV),$(TRANSLATIONS)) diff --git a/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini b/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini index 61c4685..6e55886 100644 --- a/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini +++ b/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini @@ -60,4 +60,6 @@ VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED="Error during parsing expression '%s': A VMSHIPMENT_RULES_EVALUATE_NONNUMERIC="Encountered term '%s' during evaluation, that does not evaluate to a numeric value! (Full rule: '%s')" VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR="Syntax error during evaluation, RPN is not well formed! (Full rule: '%s')" VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR="Unknown operator '%s' encountered during evaluation of rule '%s'." -VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR="Unknown error occurred during evaluation of rule '%s'." \ No newline at end of file +VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR="Unknown error occurred during evaluation of rule '%s'." +VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL="Assignments are not allows inside expressions (rule given was '%s')" +VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE="Evaluation yields unknown value while evaluating rule part '%s'." \ No newline at end of file diff --git a/releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip b/releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..41e4dc7729a16aa1bd6dfb4e9526f4d93c36923a GIT binary patch literal 17832 zcmWIWW@Zs#U|`^22+r_xDwaISQO3`}utt-CL7G8^p{O(`wK%>wBeS3&GcP?pF{Laq zFF7?uuOOo!G=!6ZIrfrFAPARMa5FHnykKTv025oIYJ+dPN&T0u?`kbf?liV2@Xb4S zaLbe(nz|DFYMIuGS2?nCUr$h-A6KxWc;D~8^U}+gikfY?{4$kIY3;n)d-3I#ztRk& zO{5y<v~1*Qx%{c_&HqdGxjU6ST?M?Bm#S{I;XQ7-@7LpvXEsgyz3k$PH}A}jtdlr< z=xEHboe?faO?r;9>|SOxZKeEH*C-dG+q=IS=bSZ3xF!}@q3)`+-1W)wq=_jz<vV?r zeOH{&ojT+E5sR7nN<U|s>mI7;YSj@>SS<0}Rm*r$jnJLnA0=jp*Uj4Q>S5i%(3s74 z<j(nYq5X#p<DNWc-IkUp#9jH`opst|i5VO>w|_T!s(*I#f|K0b^)=7^m;Mk~R;j4( z&^&$fqiZ#in~u!9p`Cg=eL~@;3Xj7tb;OrHom~8?>+p2;dhu|ho^(%<j`=lJGfs$P zUCO?HZ|2rojfO(k3!Za44Nre|-F`VzZ~E`cJ8t-2%4|R3QZ%W2S$9VLwDZaz^FHkR zoL6zL&1k8Gz4cpux4*TU^bA6iIt+v-iwHy-ot$2$_5J8qo@_Vf;{T3k{#0w`Ej<z~ z)G~R(mY5}x509^DwGq6aeNp)RmSycnbt^=lcirFiA-Tevja}*fPTdogP80f$2MQiI z<>^)7U^*eQg5SF#&PedI%HGp0VpamKJ?Hm3doA{2n5wn&LE}lLHR+xc`?L*tZ}qG- zVM;j8|18l*=2zvUvJ)>S&4`&Ix3~J*4H>D|bK>~w_gYndKWv&gId;#c!Z}agY*#5z zD7NpJ!Eo_{+iHP{cW<nzn|Qt<b+Yk_j7C|HH%nEd?pbIitq=@3)o|fq*>45AAA%(s zM<buzPYtpzS$>g2{m@N0_qu77pNw112;Bbu`Ju<o+|#8;>!qVt{wWt+cIjtfMbY2p z?+*`2&)Z*LZYI%L`*B)b;&G+mEgofEfApmd^%uQt-|5K~F7o}J$LCkI>=R#oX%TM8 zd~<&3<I4N<=YF?6f4AQHR(|234?Pp)UM^Dd)^zWA(=#E^$?H<Xy@!+4UP*FCNA6Mm z-ejkqX_Vfpbn{5#e72~@+#CK!gamHb&--ck=vKe{C&_?3WxZ==9QIyI<!w5c<{go! zo!mZSO~PJ|$$t5%Q*<WIQG4<?S)Oh32l*eSw>=KpE#GBy^5prQJ<ZP>J<l(B;wjj# zxt)`Lp7825nsdY#bw2s3Z|SxvC#hD*@1{!hiTMYvzP+~Kdg6VPZTn2NiDgdmUdO6B z)lg*8X>QKxd>zU;ewDM!qHLc3OLx_tdg8vJ$vz!j&uA@6^Y@`!PX1FD6yfr2?T`wS zk5p_FWctO~saSDf@BFrZx-*-brmk7ibNPnS)_ftR+5X)+{?C?o1h<4RW-RxbwS{Zj zj!Pfv?<p<akl!tSwz@6s)641ox*iQFmwu^Kan-$#SUX3pF(vcFLg~4DMGM?J*?H1E z@2L7KmaUDt>Z9TR>yYEpGly=!X#6u}&S}xBZjQ^GHG)(W`j!3{`kkIo*qFd**E#K) zonXVOqKT(V{2pwMiP~|mV@HS5)WGS9YSK$5W$if>;yHWb<`;8B7#=KN`M<%xqx|$e zhaYYSTM9WpeSE6kdtj}@#+o0T8_qtNkl|Qwx{9y<`fU@hic>KQi>vouJNY2jdryU8 zhosn%NauopOIsObeK(!s)!&<WMK$sELFwNzQ?)kEII_fsxAUfY_)<~(y}W_Rm)e*A znt0|E+w8-KTK4~)x`oeO-Lq$FwbM&6rZ<1KDZ9Ln`13e*HqXY*tK;Gymo87Xtq-*o zzH$G`YwcfK!fh`vSN{6TwASj?+bQRdU;DLj>pkmj^ZL&iC%%ddxP9&O#f`jQogUiS zJTNb;RQ=NAw&)(;k6tmEtqL5IPoLLkEnVgR<gl7#mhJV5#1Ao1HC2;O1&VF8**UrJ zhy2UPgKtb$i@UNV{;Zvmc0zkY;j3njKW$O55t28UC0jpF2okl)TJEjMcHd^og?SmJ zb7Y^-es}ilxmS<#t>@plJNK$@w|HJu#Lmpbm&=}(Mdg1nbLy;3NO-kyRr}7x6CJy{ z)^EFEozeX;^#5FQsd@8btMh+9PZz)U^5lnKe?#k2Yb9c;{&qk7x?aVav#F|cTerYw zfmP~@zu7F-`K+=2r{(%7=e%vyxOF4HtKBX=@nrSld#lR#SUtP%etdPV&6hCu)%RXi zlz!fR_1>$r@~e@xTXz+PnZJAW%v@Z0w^u^K+;GK%HjAV@>TQJmR`PyX;X8M?<@61C z0t+9{Tsr&ARRO7&?tZT&6Yu;|N?CGBuhuQmI3-tib4<$%|5x8On%s+77Ob^p57&{l zbxm#ZpZI@^y-IYEt(8$oop7v~g?Ecc@yp}8V&0S<J;`#EVXc2Ae_lquYUY}lzGk!X zk257=;%*<vi0+;<TlUgQIsPwVMyw@;^8@tUB|Ode_AKc5w5xQM$vKr7&L^0oN~<+L zo{ZX;x^|g<m91CE(%KH8ZOV~Wv)aNGuFC}rN}Lg0JvFx=@TO<2TW-LQZ3p{rYt3D7 zEaP(ckL<dHC&7J53%>KdVe_1NP_FXwW)HIm24&_)z29E%^!)1Iam;7u!QibP7L$*J ztec<2qxI;){Pp~I^}hN!J=Nmw?o!?&d8Os(saY~_ru=DGy~_8A|1DL4f)IJvW6svH ztPT5S{dxZ=MKby?x6VAz36IUgKOV?r^OW4QT1IG*<6oc2GuGb{NoqSMnsIff6yKd~ zC;nQ`lgbd-BBjz5bS1n~X>|d2;e)Qt61_|070bCT<Dc-Gu=Ko-VEVdl@AWM+(_Sy^ zo_LPm#6E1@<B!d6TsIypPL@`dW8#kU?YtmfX`z4dkF1PO&$<;$=IV&uNmH4%f$`5S zrWT#p&1dEds5!U9EcJZm$k}vD)Xhfav&PBRJhhAV+q925c$(S$VqZRC+p&yGX^G!1 z>`-|e(EIV`;Xh`b8TM(mJHlA^$0vUf;}N@gW|2o)G8^w5ZRPvPcAlEt_x5aiZu(x* z-Sb<9zpw53|5K`dyl7c&E-lKE7^gAE=iOz#M+ek>_3kXl{xDOdL*@E|-|gCdSJ*af zUpF&xkH!Oi?hU6yx1QT1m$kKelbseD$NEEOf7W(dij}Os&}hTKFPK=Q;e4a$_ME=p z4P6UE=g;tL-k5A^HHUYFvh1QOtHnCI_Pxt56ZAVHwCra=PT$O1Nt|nstnk?tACMIN z*wwc9z~L^-8w;`)920B(@~2&}e*dBW6WW?ixh#E9JZlEqk2#XtDm2@_O_B}LU2v6S zWyUIHHruWK4{sYTRhD;7{_^GGBE>JVyX2c~FB^Yi`|8BMS->b$OUSCpuV<U))G7Mc zRm>Lahn^9g+BQGvc?kQ8D~0nmU5mB-6KKC@X;kw?C#AH-dpE8xwv;_FVbS4CpXJ;~ zX8F7n;L>%zbL!|M5zg|4<tIdfG$wyMa=L5bmwp+$=>c7;&&!h6Zp|vKTg=wa5p?+4 z;q&Lr&;47s`!lcH?716Sm2xVME5DzVDt%4m{9WrX+q0f04Y+o&g$U29d|0`0*+SjL zJgcU=N#EklHoUe~+S9kJI6nUH`ha;y_^sNEyB;yDbFxdY-g*9V^2Cp;%Iua!9;}cE zIc>MHl;!M}zN#;~m(S~e`F&f|!~1&*)`;~*uiCuI|DCPj$BSt*^rxPDtA8uY=H-ij z+>gID7OE_aemH%;NkMR`;@OoB-s&t<3Lb1ate5orwDDTmx0WTo&FwdBCM&EriJ$c} z`SSlKSqUMhPj9+>=T^{PL-myQ6Wch~UR@cTyLj{Bzy;?bIxA&)y}$7&U00~u+o$1i zYMw#l7n55l3nw~xZaG^n+F|K(@wg!OJI9xeQ2~#(=Qx_5<g93Y7q)oy_a&>6*A+~C z{bcounT1MoZb_#8{&=E|Ww+18NBOgR?d$p*O+Dpz-CcR+;!>9<j~Ytu>`j*Zzk5RK zo&$09T65*s8{RM5eUs^Ll3ihxyq=r=9A5oj&Drl?{jIEj!t1}zt@iz+91|<)J&U*h zJ8+Ka#lt8g-^p8+N>|-Ae0*Qm=4GGi|HxIVxzqlyJal`5S^dH<-tN;(X56?I`pu;| z_5AXzMM2-WvjV3+RrlWXN=0sxlSX>D^5rV^go4|j#f^?XZd>iEdN$m4`j2MY=^D=$ z6;3^r)wtw%XH{3kSC`VD=bt;<=T5kjHF>J-hLbHW`R>gh^w*}$^`7!ve*YE6&s#tK zQ|s?Y?dULalRD=8(qZ20wy)bZUy&|tU$$yd$wy6@r(Z2&71aVxmWgKjd<-<1Aj!0E z?JiZ}S9M*MH}8p5t62toxhiw1&rxmLvfn8mUv}sQFaEdrWBhID3X7=k?e=FiweLOY z<mLGvH}9r|i~iSN!6z>(?W%M6$Zj1If8HtkY}w+H&Apvrt0lhZ$-67uU#;pYa)Bc< z&Hi$t)Fu883Z;=;=}|@&vI_&AZfocLwBK@7*ow<f(#(Iq7FJjH$uFpUJ9l1u->>}n zr{|gq+b2KJPp#jwTbr$9V(2~j3pYMoIr5<8_l3)SFQdz^oYg<L>itvOy4`j4$CvY7 z`TM~jWd7k!AJZ;r3F89kzH85Om?LMeyq%T6{W&Xc)xy}>t5+BHKg;@bxn|~+<&s<9 zoA90zxqCK7d4avr2F7DmF;|oq*&d7vXw;VZAmF(E0sp1jESZ1a#>d9SZCx0iVaXG~ zn6yj7*di`hN@by<Xv(#gsD6WWVyeRH6<(K0GNyLuR0bp_TRc?_{B7B0)w{|5&CJjH zHkq%Ha?BE1$vdTf5AT#P>5Q%ysig&*%G^GL6y1vWoz0=i$*g>d|5SgORB0#M;T6&Q zqmF6t%VxX2e{n%%ufy|u`;COZ*w2}>-+t%2>c#Qw*VXrLaGm#lp;qdYT?U>t+{~VG z$6oEZw&Yh@QO5KaQR3OJDvT{}a3o);to`|IQ}lehuf5AmB~xoewXR-G-+E_VpWs3h zb;n5x9U9(pjgm>P;w92c9uyi?bnE6S%G%GByOdWF(*8lA=DcQq0P|6Sla85xtKvo4 zlRf{kpLe+VD^yF@raJg`UCr)Qq2G?AK0g?|c6#k}uj^i~3;N%g&1}++$(C8^X0*?A z=^6g2T`wp9@tXaDG3r*uxm@Lo(IORV5C8a7@?`4S6F&;$KXm=g)NRSAvAhuUC%)<V z-v`SNK0VL;zWng>k4IK3J@<B=d&gc<ef^!RUF*v4TA9xY3siWqXP4EqO?>vNTubd4 zx_tI#nP=~{)LfI}&d|Mc@Ad4jQBRMaob0Z(($de<Tz=^lnI=EmRcDRuFCTBO+;pS+ z$=;rA|37_OzU4Nj+VOJce?_&HHt*|S%(uVIV)|-NaGI0A>irHrU3dK4Y|5t=X|BHV z-|XVPGwZKJ9tvSIntx{gp~@ALe*D>MS@!c#z09HdcUK(Ghnh{_X`lOtAJLU>$@X(P zFg?y=mNWx{d@2KjIA&KqDX};e-i?pAArlC~Xx;cdHZQAgTTI&jcDcize@>d(r_bHZ zcAxicp^DdUsc646^OMV$xq3WmQ4*E2OPkDo>-i1w+p7+F3T;X)&wsu_m1%`mi0CT@ zuA0x=Z>DV9Bl96p;&X%Po`dc0>~~eiO;*+9cDnb$uX5V*hX(Co^%hSepP6o-HvRg1 zo5(c(8#h(v_*Tx@92wGcct&r+l^MrvzAhH??(Rs>7O!J-Hoj??y;0-OvB*=$U7sw^ znU=Cseq+z_>52^L;fZ#Mhf}P&f0**lYW*l7mtx6m)Rh>w#D@7y$Dixo^U@N9HeZd~ z;KiTz@55~SXK#{cJY%*0a(Rwi`q9=bOGPW=ZOb$l$sJB9o80!;^W3*D%a6rPw!M9$ zV&k`*$1jb!RbCg@b1{@H>Igi~lh!Q!!0|U<g8%%@!E(nYt8m@Ox2u!*C?cUc|L-qf z+iYfTjxd&(PUCZGm3Fsd;`i^`mDT@v(cC4EOI?IN2JW+)=la&uuJ^|7#h<^HtF!AS zsHd~<-TUrh<o4cAtR7;26OXxRemOBIu<q&dJ!gLjS1j9M`8EH>k^N8JG!_4t#_~{u zh4WmOiFVDw^)`t=Ta@j-`FYHqD<bZoZ7uL++YI^UeJKl^R@jQOuJY$t@i;MrMd7)M zt3`w9gUpJT&lMkdyS>r!+@I>>XO-Z}bGCfJqy<7Rrgl{}@~Igs*m`%BWGq{GAWdbd z@rxbr*e@Hnas9RNydL;=lEhRIdp^C+<lqN4e%<?cvVPvTdwbt2n|q$@?Rl)Ce)cFQ zyUq8*9Rdn`nkLK~Pu-HfcGoZk#GF#N#(jcudF@fb;|ViF3^xdFc%iB&e&;Cf?-%X| zoX=Iw{IC0X<AJ^VWcnf=Uo`l%Ip@Io#6C%#cc)*ToEgCwxB2A1>PpS0_qz?A_VTM+ z|NY+F-`+gA`E&W+#m3jte*a*0J7d(ywY<;!|8n6^I#Ts>w6Y7jxa$klJZ0Y;>fw-& zj*Cw}URc81)AUh6XpXXD6DNP=?z?KI51o6{I)C9^&r1yZn;&_8R$9Z|Iccuw=EzBl zx@x~%zOcWc&G%bp#Py{ASFJNQ&ebg1aMbUT3WG9}h`p*|QN2Z?@y88Ef2d~gp2>@H zspJsjFyZ{t&+f}Gp=k=Eo^p$Iq*!0F%Mm?ZH`mf`*`L9FP6EZdyDfX-pWE;yaAy5% zD3~_=!U;Xj-P4**dg}c<*=`xlTHGf8M$O(g@#?0li9-E{47aV(*YEABnDMwvu7T<3 zp_)y{l6e;_{WV!jhfzf#zj*VN-S@<M{$IM3#V3}WTP>ja*l^>*!@rh4zpJ}i^Z4`4 z`G44M?J@X}mAiMrJty9LXE{b=tE4by2F6d5WRKarPoBSdzx;{3-n7XK4GNp;ih@#v zB{Hk(P5PyMxr2{=ww-l>(`dr;1;G~lt?3GBx37O$;L*s*#?ACQlQUEQX`P<QtTe}8 ztp~0Iu^wgOmQY#Xl3;v;_q?R;d^0}rLsQZi!@m6g*S~j3t4;c$cbOX>y;<fI%Tjby za)zd-$<qgV$wzw<m-a88YraxtvDkX6wehL~lUzeL##~Z3v-(3yh)^=~JiUcmZvL^K zn|q<%BHt!=)91yT@9kdv&d%EE-R8xgo0+#2-?Nwd()Z18cl+JN`>rch9NWT|v6jz^ z!^DPrfs97g@uOdwmX}T{cu??YdGGV{vr2umiWt_;RAF3rgfUpI<>(ft{W5FYc#;AZ zOWDcrcDYK#=jG&R%*l9kM_Fd^tfezKk3XCzDRIBxm3!}U>9yid1NUtyx?1RFu=L%f zf_ryk{o{XaR^rpW#j|~O&i>zJ&;KkqzprBd-rtWx+ZS6G6rG<nX<Ed)c>ZUvrTb1L zc&Ko;DyGO?nRSsXCX!!7i}Pf#0_#2Fgm7UVbG4JAKUZ{?$Y{wltZ0|(yQJb~?aw{$ zYY|Uh+C=_qywO)xu5Xg`Nn_!*{ZM@0i0{xP=AAV>8^UE*@gLE=viI+9wyRraxTV`X z5_!Ek&Gz~Mwhl*GAJLf3wKvpc71%Ro_h(k{I`ngO%)F@gs)S+LyXQQ%{p|bOB-pn_ z@co-$dha_w|170-ySa?(mbYH%ZU1L#&7&M~^QO#cPrjcPcFdi!%DeBCnLgzePmbPR zyzZ#nbV<eD1sisE_C9a?p#31$s`H?CUdSc+r$!}WT!xJabC`bdIK|54EYxTE9DB`H z^Nnd!)pEH<Mp0{D%hm0CAhLKFr&^CqfvcgjkE;tqN98Q#8G<d!F#$bHPIey^n(wFt zWM|cv&zp5|)|s_etDbTnUmdb1wWQl|OK`A3-1h^itc!jqvTpX@l*&6f>FvWi8Ev|j zy=tr_-Xd1!VXCf6EnKTUIbXaYaM-T;Qd>p5ef93z_pfh#%k8=(61aC;(hH025-fME zH=gL%36n5m;rx`!B-FgnUUaIJpU~Za<GTc>XS=*Qxh>3FZ<f}N`|o7t1P1&FcF71l zxcR)_^1q@pHy)UBwLw=p;pEKwTqob{G;LqDt2%zlj$0>$x{7!{T=kIek#7)t<bHVN z&)Yh^<`)@yLzOP{%j!%FJaB!|vWeTe-f$Rrs)k<BVZU3#aD}mnd5PadmVoO8vIZ7y zv#*>nyd&u_<wk<(4)>rp0a8u=UbU%50$!=iyV=Qn%YW*!uLm|R+bI9GxzpvcUxQ1R z%Jc@G?te=J9vzaetM*`NOf1<R>gCQ5F32hpA72pmU>S4Dbs+_-mzu(i0o|#JD$gGn z?BU5N6?yp0Wny}wg3mJlx$+x}H!Qxiao%|bWA%;i$`n;O6z4yw`|v`aFKx;G6)r}p z8JA=?hBjU7cIUJf>(9^@`pFe#{dmEh1-GK++T42oF~jnoJ-Zd3eMz{@=HJI3NhTgy zx_;H}#=FuFmsB}Q{z`i1QOSI3<C;iD*YDLirI+HmE(v;=CW;o7^6GMWwQT6?DGRh} zagXQKOAwR4*W@wzOIooo!<+YVtM|6O-*L3?n(d2sUm3sNzWmtv+wSAH)@%`UT39^0 za=-Bzr)du^FaLh`vfl5zjbF=IIvAe)Jo2FFo7um(4`0|g{`-}91TJcDx>Ar}{jFQb ze!{7H7dGvhQ}?OSc1BRstQz(h?GM{^-449jl{srk`^|{lJsOb}^90s9h2K=TqaAeN z@58hE3=HN4_MYN<a_(Q^`xWKa?C-vmpZ}`%1ZRH&Z`!;%gDD@D91Pk$c~Vp8Ri5Cr z32K604=DW1xN>Elz!Dx=kN8P9qgQfDetrG>`fl^RwP_X0ZJOlN4<7npAl-3cYvcLZ zZ}NHS>%SdH<70m!bzm#QOQ(jZ%MM;?4>fL<o4uxOP2cLkU$+vo{RQv0+eu70dzSIj z?;iypHkRMAe=qlL&Np`T<;Q<Num0;;|6IO#@#DqouDjT<wcWTnIr~zza@hutmbs$0 zRQnzJv~PR-JT<){)%$ziZEuP2)71uFG8#p)@9y=Rto`QE+<EdP=h@QVSF5Y<U0mM0 zc<$kK*EdN9c-M$E|5(|5`)g`Ina`4^0TXw29Caxv;oIoq`DvZTlARX53hnpgY6=3E zGHEZ#p8I9-E$5?t-H&fu+bQ#{*t%hxt@mNY@TkXgB#*g#pCPnDILV+YMDEqzNOj*$ zUyh~jnSP>b^E?X~rTN`~y2^(&l;z(ZeW_@9(B$ONxYlj=`aGgstc?_>&#=A}anfyL z`9gK88L59~OGc&$=G~5Ep2vGPcB5;_QU6Q}<t`De69)x2cB$PF>isGDdqMd;(W%+D zxq4sRFi~Ed;*%C_GQmWOPiaH6+8YJllV9#_ot<&<eu=tB*EA!Bx9&emrrkGjS-t5| z+h(Colhf=hA*Ev7ey$8=8(#gH*eX@lIzNBYOnL3wAw|o!X*^*_+%)II#@Dk;S+=ds zjB?%Uys*6GMvTd;meW3Qyitk0Q_J=yS1;YU$nZ>08Pj%^`?tD%xE;^V?B`K5QO&hA z{&c5JzU)JahM;1IZAa1Dj2myK+8br=TFdSvF+uW_*F_1IuYCbI{eAzR<*wn#St}GE zyvu9l%}k?JtDk5EOfM8XcIl<N<x{tY2U9l()pYH!4{8ecovp2+!106qp}nkrSjK_i zk5@xh@d@pkwJx{$v%KE~uGYwx*Md|9Rp)Lz%(S|lL5Sx>S<-&5-PhbXU2>%&Vpm*n z+H$Dps-kM=!9{D0JY-+-yNI+lzja@<J!n_fC*STz%OBi$YVtt*=#Gb(7prq^9ae0$ zv=2_KdAZ`o=FQE$UTFaVVOl{}6Pcrfl6$TDA4Vv9I!iorKRfO5i+QF6|0FJoPV|4s z^6rPfY3qE2`7ITjB7K{3@7~m@{rx6&!oO7YA5V|$U%2P9w(7j6amE+f4lIm3W7J>~ zIU`|ncvteRr!t&ZC7B$Lxy_j#wPc&4u%Fiwzt^X)y*hKG$ztM}V*QK9&E{6^yivVy zs?Nbj_o};o{I8gopx~F~*_6W^SsuY#Y8!Z=+I7X#G=nv3mUJ+06;GKT>djoXdS`~A z_nCPvi|3TIiyjV~k!HDiM|JXwDN<MN{&t^PT(V@z(~EoSb$KRQ&CCt6jlKCK!@S&j z6XR2(&)d$oDDPJdEqiwAzuAUW1ssmYUhUlI@@IF+Y|&=UC2KntB>NS`JnA^tUA{?2 zQm*Ah#bUz~DyxnP{+#|nGiP@0jc0dK#kQKx-@L1{;N+S0<(b!=oYrTGE^3b`@@ER0 z%60va$Hkv2v&`HUOElgOF_DgHs9yeO(qW<e2Qxd1Shr7B;R#M(m#clg?(Mppv(4(x zw!5DDe&&*Ck>>hMw<TPK4UG%^%2?|B&wW#W{Nd+$(HFVFN#ExO-(7ac->cX&_l|Q5 z|I*LT_xAQCoqyiDe(7YHyPaYiRJXM*RXh9a(V5#3ONu*x@y?X|Q<Ao2QO0qWRT6qZ zy3=$xL{65kyu)7XExPlez;_Nw>9Z?-O^S{_`SgIqN5$;S&0F}`V)qojG7>#C@6eVs zO?iF=2F}mNPE|ej{-ye_@&Bb~zr4C54LqNzg-m<=Rzk8TEMLZ%-|bzeYhuqyXU$Xo z2R(TwTkt2Vt_wQz_oh{T)P%)uT7El2KC@q2VQ^?>T1au@a$ReMX)HJRTltOs-*9cS zPOvXNSQpm(_l@F)`MVSB*B`9=CvZbQW<&gyg#8s9Z>H<r(9hlwZ^!aYTl`J9>5cVr zOux5se|s(U<~o1l-)`=2`ldJbPf6gu92hP7b#X#?oI==%U&0-(ZNh@>9V?_hiOhH0 z!`oaMtGm?NS$0c{rT2ygiPNp}7wZmAlG(6od%#j(UiVX0E2b@DXqzn3$5Za|blt0O zZj;ya8oEB^OPTaUT=s0{l>hfG_GaI{ztP9;OupC4wWsQ@PS`zjhrCnJ^gy|v;hO@> zxkQvhy%%u4icp<9-7_SiHHrODLSTjY*Y)1~GtGIEU!SahVZW>X$m2r0jlZM)W&W-| z#2~%)->1&qzg@zdXT7^N*{gmoZ{(a8C1yTCOXThD?%IBcL(d>O`bm4^41*NAuWGYq zZ+7)@ZYy`Qn?E&V-=s9JHU7_DW>l^al&|vr!4^FG^1Yx9v-K>ZUa+Y5tS<2R{kC9B z)tRO4Tem;^RLa+AmLT59&$33OMDO6Ho2=7jJ)4m2SQ_|SVMSHx8P%|@yB~CIvwU)^ zA@++N*N!Y_-Wj{suF%x}#htri8>4CXlD}t})}3H`{BCDxVo%lv|Epz-G<GH(_LN|K zRKj>Cbe@4d$A|ikGfijw;*%#{7dyfkb1dYb=iGw^73)uYO38ZPeg5W3{kKeZVW~kU zs>`nh-^|_r{oj6#Ym-g$_}AAqYc8{K{$?)n`uwkoTl*Z_e?K~zH$T_t?kcnY*;)bI z{_h@|^B8P+cIdT^K-Hf$b5c0wzdbqYz`1Q_mxwK9Ju~C``hvFHqF3`@{tmyqhMQqy z|DT+w(pvc~tIq4%PI;G*czMs8-`Pvo-!a_(e!`Lyv5Q}9zsl!)HDXV(`h!sRnPSbJ z_OspN*EUN^w@B)y%FPKmv65-#{mTxUB{mB5D@?gx=zVa@js?n}YCX^E&hWbXs(4=W zu1h(`LRq&pIA}|hTdp(GdBnLmL+??MzPGN6RY`CZ|F>_GGM4qq2wPp7b|>M~WyADm zf7XPWdXz2Ezcb0|r~QM<qb1Sp5p2Q_GanueH+j8-amm}ACz)m0c0V+^@U+?PY4jE6 z%CHz)t$#(=BRB3bD7c-tQRmP8-&=ktc`o`nkJIMohud*N+|SFde>^R3Z?KNzZ=?FL z`cH))5A!#lZauB`>(cVgq5tj`d02COzBF;oE`blvW9Numz2fz4ra)a=@{x(^Q?<1n zX0F-OI-|Gy;`|M7ez7UEmz1Y@h#zucG?V+oS|_z)S0cx(HqoHXt?Tlv^!sIwr$>1B zJ?{=&a?02BWl(&a>)+*T-UW#6eENHiC+DF%6Qj?4y76;$=vyW6z!LYZhErMIEAIE* zTFCk?S^Jb;aJ*Fh)jzkl2q{(e^tu=3?=D_%9l7-NvzmAgz0K$MC(dlX^(t#qX=TYh zk?jZ9*VcS=`PvXSy+fE`R(R0v#<i<2edXPC&u-sE20?GuAYIeoW3>@^nT=T|H#Nm= zx|me*sKwu{PU2y-ew2#v>$u5RUcK6@w(!Pr!^aUTxewa~oP75z?Nz(v@keo2KCqqH ze#K$_>>Mt=L;D0K9L)Nh$QJpmf9K~_MO*Fu?)(11i)r3!&r@|--^vQKpBst_eoLC( z#%dOm+;DcUy6Lxl+DBfldA0A=IVKIG{idluHoiFZxY6$P(igAwu5mVLt4j7~vz#?F z^_%i$8p9%%%Ud_}CrACZW^GZKVK)Ef>16*IA32w-3%1`kaaZ3&z9kQ*Z&12$VQXam zlA?k=zJ`iheQ$~$Dcqv+!c=X`7j;g7%W1JTCsQ(**sfo{p2KFP>)W-Ca}ldUTubL6 z?uT_wg~!uoEYXx=);SfOV`#CJQ*ujY&!k*Go!{Ln9v_K&u58nnyOFob+f^xUf#Bme zvU6vMt~!*mQB29?&88ZqultV%?mW6>#;%*I61e=XwBEG!DhLv8zUhC{tU79+{h~gv z>{~CF70fHUHDmj`YSZq03@c>zH{4yuaIaFf`_F^|{a0~so5FXq^Y7wIeP_2)asu!5 ziy@0N=axr{?VhGN?fSHJ7AvBDzI)@mUT^Bu)4Qa3*;-#^z0sbfn;lmC=F*|VOP6Tx zbKleM#^N8+KR42RdD6#o{DlXlU7wX0mkO*&eAjufg1e_7$a~=(zF_gk4;Ofq-g6E4 zF5|~H-?2~gkdx!WS#i#1R(y?M$nHArqosI2?e)7Qd+us;X@<FqW-aaRwAgsPXy<}8 zF+1I&=db&H^s1Ek?DFD#vFKFU2?pOUOZP-+L?_p�wvh>M)x9a9Zxf?gJ}2|9<%9 zY;|>B@?|aA38BBzD>u07e+*2``pg%~YJX&-U2bMK`?HAqRT}+YUv`!z>=yWtuJ_=4 z)`9u996yT1KRh>ma9*C}Pb>G2ZmAFA{7nBYvHnTr{c)O|vHmI3zg291t~NL9FJr8a zW&Zce@j!hc<Nu{>f2La%L}bW*UbkctZ(!KEl$$M*H#c3=Xu6PUa{9FW!}Xu<CYEiS zRO3IRrf}NbChtF&{NLVEEZyBEH@kev&C<13mUurm_etMd9r${!_Nlf1rV5q+jk)E$ zDCr*e_KpWtt${D+t=(mH!(ea8&9yqwo0g?pNpfe*zHOaYbMHqKdvY1q+0f=&Z@&wD z&P==NB^+54=3?|}`_G`a30cP$sGhg_QzfR#m|qb+Pe-VDN_${<p;yqnd2M{ZW|}X! zy6)j66Q2X3E-`x!`re)J?eerWUnggzS6x@y%~kBeJNevXjh%;XM_q_7EbCaG|9Q%( zirHFP8>V=O39g#frM35{Nqu+h0iON2R}~+rmc01EaA3#6fOc>3TP6li->E8GwtS$x zzeB-QGc^1CnY0OgT~@cB-7@RnFD}DWztQ}{gz5g9CEb!1w|)O8u)g%s>0NA(Ha+-x zG2i|8{604Ma=&Wp-Rvb7gEz!&-gDHZ=v<s<nI5l@k>2Sn+4lmczN@5E6lmYexY_h0 z^oeaq{|SW!qATx+w6#e(9R2+_;{B|X*Td2x^c=O=Ef0n4?fLlNujJPGhw^)tCx8BC z@3y{W$!?~d7F=dqejHT2r@+BuDKR}l%wrDAVqF<0r^16bEslr_Hnshp@XI-VkJK5> ztsj-yvRV9<CP;E!)MI(TRkh4alX1#bQ-8}B->#~(SbFNnRIu1Q)Tq(=-k1|m`||M? z&OfUj_S!ksFi-k?ujGUJ-y6&1A6eJF>yZ2Mb7IB4iQ9!H-&A-ioB!AQ+mesVJ}=(< zH+gp5zMKC(a@dvB?fH4}=f=A&&q@pR13M)yG~fNXKW*K%rM(Iq^?r3npE(p?bm^2| zEY9&Ys^tnx_OGSAj=5Tlrbi=IB>byRPfIUPe`hv*+m--<z1HPv@p;c?rk8*F=D4SL zd*Jc6jHTzJ|Kv{a-F;|D!P0MwAE_`(E52EmYjt;<Wc-u&Y4XPoZ$5o9)6S;azS>wK z-cPS6UQqp^XR7w;T~c*R(zzUp40+n8q%zh;OYQtwS<dyCMS1BLH-^)hje0juFJAd& z5r;nC>94s9wpQ3(*}RjT+adgbaj>T^Gk0^ZLT5nj+uiGKC47CczqtJDvE;rvbAQCR za9FR)yVZWW(XS&<e(l@VzGZpb^=H%WJ}9mEDtqIb{MP0Bzv(;Vo1A;;y>wZ9`rVgl zPgZlD7L|Yb;n|O^>Y=O&SD2gM#j_kUl6dJn?LSN7p++e?r=Y7#KdirSRBTgW`O+Bz zdbJz5Y+o);zg4*6$KgMm7yg;M%s80A`C-x@_I<C98q4z~O_$z(=Dj#SH_M6H;+JhK zEHadCsTWM#F=tLFOB>g_-#c%btY7bD<9T3Vd3mqKmS1(urXM&;c3oh4Zk(r++NdYC zaizt5&UzEA%P+Q={K`w=p7rwK@}QS1FPvwpzs&uqzxiQG!Ax#GgE<0>3CbFqPEWG$ zNWC~=>Xi3W5|@5(bMNtLS$;a-PW`;ZgEv#muG|sbtNX!E$9+oiJK?oQcx<kCUv7Jp z_o1IT!)mvirTikN`W)sT&a4`btUHYjb0nTNzgW3=$p!iAQr_<vVyhlq>(S#Wuod3r zSbHSzf!DOeqndwRC-2RZoxOEQ<C=_ux>cfwnJ=A@n%9~2FUa%(yY%Wv8ztkTFI+UU zd9N)HIubFx+d*@>o6lC(mY{_{T}-aMIwX0ab1IVp+rgsZh6i7IcAO9Jj`UR9I%P^q zq>sjGF{V>0?a~z?F7At?+~Z%LSY-7py2Pb@ZN6D=w1Dro$unN;Te(XArAw>TRj2B} zg?qCW9$s-Faq9)Ykc{O~FSN8uwt9Bd{k%DSI?uWN*SK|(SPvxM`&r}IZd){^x+UYN z;U-`6zLgIPU;mdrC!VnJMA4sj(e+L>T0ffOW?Y}J#&Xwz^riRn*Km3zI^0i^+i=#? z(YkcW-1uCd6^Ug*e$VF3u>Ja~^?mDCA)Z>Zo`M?!C9{$?-!Bteu`%Yt#O?>@w5~{b zPAm}?y5Baz;=*SC*~+oE)|ubAbbr~2veinnxpEY)9x|9@_F~fk>n*J7UvFF%Alxuf zFScaM@lDr`#C~j@>c8?v3ZLp_&!wCf_<x<dbmyLa_!jmTzt;T|Mojf36`DE49BW~| z&cwjb!^Oa$ggMm*U$tADT$EW*0-yDJ(f=<XT1+MoZPxE?Xl(v11CiS2;W36LPFtJ4 zalPKQZ3DkaXWydS{FR;g9jlLg`*W~s*T3(v>WyK47tFnuSsNjDVBfa)w$Fb&dvaBJ z?n}1gAs$Zxt>yan_43`84Pe`GV1=#g>mCo8q6E*TYul`w-IPzx+`8cN(dmyYgm*c- zTxRDk-9A&SNbvn<)ybTn`Y%~cd!ErJth^<Aj(Nkpd*}Rq@>>_I$Y}rA8N7^dZTP*c zv<Sz;AMMg_Ez~Ry3~CTtt;DdI$MLMcVPV3BEHAZZmQG$jdZ$lX&~UniHG1m$pzl(9 z<18=L&Ff|f(ic{r(qiGK_t3@s)w<J@SPGim6HX@fg-A7?6x}gZEl2aESkZk~y%XLF z3sa|*yS&X-o3zG`nZ+hKD*2?c(9~)6dt>@{Z0L+y)cM-s`TOkcD-$kMDl<OwdU_(m zz<Ay#&kg%>ru^P=jqP%bj*)oimH0bHelhHglv?p;`tQ1(Q%n07ln1O-I=}7YAC{6G zzAaL}rkU@y37Z+B>A)Xg<mI%W<ej)v-n6J1zUuRdfBINAM|}OwU2?eDQ{>3*JDYyr zTJ-zP#WjnzU7r(p-nKj0rr2Xkr`3g`(554Io2P$JcK+1AY3BC@kymqqnu`y}G+%7n zpn3M%y0g=+rbWN?4n60Yeruaf&f9Zmzm-PZ_x1^2G~01)!G*0+dv9Fh6<w)*hrN6c zzv#Q%xdHMv@%)F1wYb*ybT6n~6*6&ip4JPsuxp8de77%19=OW9R$%FF3Hi6@?T%bN znZgz;A$Qq-@<%5R$F?IY>SxU^IQQhxX4W{jfOq@scbL7sbljZp`|1<xeb(^A?)-98 z#&`OI63xQm0?~F4MXCMDI)Clv1qMn_Jz*ugV)2|?-CtHL>ObH3_+3Fs?k@qp4-qP_ z>IzfCuYY{otNXw6;@!L$X@mW#vB5eoibJKvcO)3qC&V9q`Gh0wH~W0I0w=j`BF8Fq zW)yzBmHMb)VR^;nlL@<7V@>qx7!g_K+eTlflZ7j7jxaMYXo%xq=w6YV177Ifz$6n0 z!f1Kr>WtX@+h!td>w_2kZ+zfuw#D$Tp!G3@l^(*^4WFqwCYA4=b-=6V?9@dplQvEM z`)lGNmD8PfGh7=_nkh_dZ*OmZU#uZrWM|{Xv!-K$$uqq-TW$#m$Q@C9$}N5_eZF6t zK+T7H`wBItb8XhWZ^!aVQ&G<+V%Gwhrq+(jM_N~V=w%8Cu2bmfh&r~A`*Mn6fyV+q z)-T5&S}5I{e*Sy-<lE;Lzn)odKELnX-_NJ@`A)SJZFdk@^QCBdgy8zy@4oomx;y#X z1;=RSoyLA&qYmv>o}!;D)^vYkrj5YE1vMF`3-xAf57GL`yYOq(-Cto9+nX%3(j?|9 zO|_^LD%luv&-EhH{+K^pK4I?<u5&dv{v^=D*5duBH-CA;)#~R94o&EPeyi$k*zT)k z0T<7@J1a3e3U7Wr_tekni+^*ohu-}(lYfffN^AGX(=WJnjKsS__<y#AFPiw<A|_F8 zf<RR@ThvLT!T`ZE?Ps4g<2S18zO&WKrGn?PYnQl7f0T1-k3rHZ_poD|Y8NCahn=-) zV!3v@wUssPV3%A##C4;G>W}*`oX*qOy-jG_2lK73-rqKS8Z!A*^WQ5cK6&oz$TN?6 zw{}Csvb6uzCnLmHR~-HOR{HbVCY6W>9!xTAoMGPLO3|!BxkawkixxL=HZfHGR(@n~ z$$FAkf+%+_(;}5;H%;z-2@hE@#U)~e(Hz&QttaKxR=>!feNBpcUGRq2xxYK|dFoYp znkLvhn4<ap$I-?ebI%>0oL#Kf$Ed7*sHAd6bh`GviJIQx)0RZ+=}^@%KKY8bDaTMe z?AniH6Uq3qf4XP?{Q4$u(e&B(dh*{b3|;z1CiGlV@5`Hvb&}0oLK%khx9l=8GHqP) zE~s|>i%7e|_+<&AO_j_|s=S@?v#fVEX8SGsUbD~5Q~1*g+pqr23{MObGFRCC|Fr#J zgyQuhHt+vT3EmobQEIL4rTQ~QZ+WdEyzk#J*gx;gnJcq4g{4In=Dz&*JZYDdZ+@=k z--HIErG56c|1D<8y}zK>5xkD)`Z>2<mzHgv{_DF+@oKpx;UzWOe$~C4XPI~9*15ok z?(^HGvTQx5vR!@qUdglP{@3V=zfIbFLoxK*1g*^hyKFZ8vu9}RxPGaXnWJgO%VQh< z&K248I@|NcYqj3+i}mi8*SYS`wpV?YcllzubmPA2gcsGO7rxKx+jpJg%U)*Z`Y7hV znU^olH)HzyvT@=2>kjwxt$x)hU--_?^LGK;FUQLl`K21`3KCwN&st!gZS`xj+X8!Q zgD;1rUbOqV+vl+SdicNL-kyrh`dc<x6$Jb{7ybPoE2v=LVgP}=Q+1p`Gz@bw$S`E) zrKDEqWt2e6fk_KLVyiPjW<Yk)EHCnN+H_9(Vm~_r!xAY5273k>hMdH_^wPxiRQ=RE zU3Vu4saKGb9$%JQoRL|Oo0?Y=k87t*W?m+^06O(VCa~cNW@R1mI`6id*gtuN_XUew zg?CR?z1?}+L5Nj@?=i<z4M*|aySrnPUHX<du8^E&JY~_pH=n<({H0=>?$H(G8?L#F zTW7;J{+Tl8exCblbh+jBKh--0CzHhvRcJ2qIKk?YxJlsC&&_Hvt23m#k6t=(@~h^7 zr$3KAeJv<nwDJ(czVt5!&kK325;@A|x6R0wTVENV{A=EaomXV09dqfes@;6^ikQ*L zl@)>fm;HC`>o|1l(i*27AD4Zdo!I}vCe^}3XUznrXG%|`RGu?Rup0BtQe;n%S~i(K zJ^vvO=YzWo{{Oj|TmNUH^_$)Ip46GV-I-jlz{zT!=&iHc47>!^e=7L?x4OK>S0(kn z)%$$zEq)u)F1btY;}G-wzR<gjr~2~jb<abhHk&D~+hJmmEG4PI*?q>*@ZsT;ZIhR- z7C5TqQL}jUfiSI!&qS2un*IAfetV;?x-;);`dg#MpfZ)~^>b21ZJ)J!I+!JHG&n74 zeu({_<)IT7oKE^I6IyAvYp>!d;SSS`&AdB5XC3?F-!1X5neETpy??Wvjxagp$GFa) zy&=i)Sk_}xyZu?)r%iu*Q0lP%2i_HyGgSTOGCf{v@}_uopY%NYY}LfwqFvig|50sm zD!Rg8&%H9RG_Q1-sO}a;T>+2JTTR(-aoMvRS|BGiV~NC-B*l{zQljp?>4%Q!+-vG$ zHsQ^^Xp>}UoDtC@csBe{xZ<SAvRM}nxhPEhP~LP&sd;C|R>!?P`&HhBNi9@#JTJhR z+Hl^<o>TBr){4&pU#6Pv+SZ<NdD4qOhAH{Y_dG%)9yVQgk>2_*+3o0y$I};PhBemB z)v|awySZO~PRZA&hw9=}?w98;57QEho}Ievra03vPmz@~zPR7ZPC1iiwxWh-o3V-2 zG{z-S#oSjG`06I#l9f(aYh`z-$~1oe=7m2B{>tBdc=%|d*!tc21xk~z^2j#(@g9-s z7H_SUySCi=kYA6Z^ru=U>8C4Nwm$0Toa{Cu*RgQ*p_HTFze%SWBwZ`B<ad)6S#G9f zv}xaj>M0%5)_vU(Gvn#QTXQ+;_J8?!K_W!ZTxM?H|J)RVl-LNHul9jKS`X@bB62Ob zJ@)WiTql1d;9*#g^-OiPiEAcZ=;)Y~+Ig~hnI_Zg{zC?vkH#EbeTVD+p0_=_=9b%j zk3W~Y+`KgSrN**vpR~We@#}0-yKDQ{EOM!7S=;N4%+ptNTya?M5%!}+Wp$Fl3yqZM z5T?yDrqxclYUOoe>!G;N3M>7e)d$!$Gh9PD8P`}SaH(bb71uuaFY|^kQ*CDSDTD4s zT`OLkHo7BqLuga`t0l`6_a|QG`6=_at!eI*f_t~Mrrx~Yx8ttVdPeOz6#;UoJ5-~M z4qSP(@q^~8$QesQy^{`PFP`%D+9c_UD<Qi(Rwh>-s}H>Wcj0CEZ!vS2gU&tK$`JQ& zhQ{TR$r9P&zj~%6aGWvR#+R+w`E^-Yso%zF;wjN#CTEhjGW}_~DiYj$iB)vJh5h=d z=Vy}lg@?b5`}6Jf`TYU;cUG`0pD&re<yGKrQ|_t%JC8ir-BBF$a8^hD>CC88F9aQD z?Vfm0@nY0wi={subUyq0CZ%U8s^%T4l#^n$-y~{w`Vyc2UX$1F{r`ShIp6N<<>ht1 zuK54A|E>A2ELeWSpDSHgr}rh@cw;c7Yq6y0#KT235#0V4O>XbmG5Kisryg~8o2OId zeh7S>tu-g~;cKlct3QN_{>$AMSjgjSVlwO2E)8x4v*6czO@8a2WevY_cCo?5g2vn% z`vU4cUtjtE;y}Sy-jlz+uAA|%^X9^u91TgCb?m|UMz3}~?*1M3eD3Yd;w+h$)fcCv z*KX2^u_@cT;qVTduEI)@$6o_`uDcuFlQ~*ZEb{o3#<8n`J^B4fHPs@IcQ2N>FMG72 zTIBKDK%V7Ax!=1#S+BoqSN-{E$T6EI`#hr^{oGG(iR79ib(u}9Eo<`mj47KV?_HRy z^zM6M)PYNu{@j}Ju4&PCMRQ*ryRu?2$7=OML5a6NuAItg@$%1ukPQL$pLB(#%^AeD zEalq5D*jUTNweidDUQjv_q6F2{F^j0A<|k?^w2-6sqXoF-PxYcC(d@Rk}caiyL$cQ z@{4DFhRTVy86C0SUj4c7Q3%_%T}v;=znuFt<I>)3hn_xNHqXEQ$|c>(o%fGVwl!RL z(d=oH|K9g|56&&0-!JQzs%7uFb>i+z{Ok8!THi5q?WZq6#|(~3^4twfH<A#w_-U~3 z@Wq`W^&A#&dXBxT-ES>7Ir8J#uXDPjk8_BqY}~7me#7o#{l&*R5zK6Y({HwI*_wFz z7^i|X!?ThPRUa+9Prte+?elJZb@RsG|GamW@%_lnFmPzATk*oke9<|f_S@o5;|enE z-uT3%yso^w&-zu;lAT7f=aRX~`jVHLCaAG3Pi&F2w!6V5tue3Y%)Wxz^*_H%I;W@q z;ED63-nG2k<xy$vb4?!aub$ETF2m)Y5C7}~-+MReAI*4iF~>#eM&?|#M7cka)8eJv zYtCEnw=Dc~pEW%5nEiw4nlp{Pw}$MSf24k@GrRJSW1HjEm}O?<t$bAejqgT@g2HM; ztvyq&MX~%TDB8T{QkGle*QaS)p7Zf6b^5UOL860l&b+54my>?Fv+b<<`SZ@XBP*Ns zBXu%B?SOhY#iVx(3=CC_3=GZ`wgZYQi}f<`w*_h^IQlm`2(-TU{Usk)b%}XFg<-CZ z!qF_2oCO8B3w);gUQ0c!r~3ChlkJwyW4Be_*XBPv%T@VA=_N~K>cW*yQN>Glqy#KV zjyadE{iD}v`<$z6<^OCS*xBsz=PZmgUvwzlkk36Ux56pmQ|6W>D{TW)`)f|{tC%h{ zbJLYw{=Rzl$#Z#8&pNu7$S!M%ot@w4JI8W%u0>H{GRJAnY2JOx;!6}WEiRd8aPE>Y zTX5k-S^hQSHH-&dhnviQ8u;tq#@?oi?f#n#JU%uZjkvsPoA&wO%)LL}-do>%MSkLb zll-~ym-oC1IdS;y<$v9>7jD0hM>M$_cj-Ba?Bj0z#l*m{o{fP)gh7TOH77MUHLs*t zzbHE=F)1}i54K}@Qp*=?4Y=Nr+j*BA1ZvBc{5ia$YlHAoKFtr0wq-JhIB*DRG0Fvq z=DsP+P2Ih%?7FY^mHPCu>pOQ`=-P8+-s_ph$BT=?(o%PuN*vhD^H}r+Q@{!@r58Hk z6Q0R5{=06umur)vM~GC{6}#n=?mpNxVRf1IK@;&AEdQRouJ~8@vFy*=L+_mw6;B$z zklP-8Wy$sI?F-nGALMk_Oi=U=U@F?<xbU~gO0RS83>o`Y+NId+U)EI{pen2+etp|h zD{YQTR{kYohwdcw%xq9~TB>2Vw2doyt~gUnC2P+E<>$}t@A%j^i(?sgCBM}BEsZ7y zACk^ZX*${3T>P^#%OPlo+r&3--d=lOc6#&T3fmRE3LWgp8;%u=9bYje{FIZhn9rQA z(>%`yM_p1m=3r}^{7T2iXik%U_%!kRlb$Y}T2Q=X#p5-3A|*Zr@>bh>!oU1qq}+96 z&+*m^n;vWno4$$HWBp?{t5$}bI}bZDU+rIGvg*Kgzgrh2cO1XWY+qxg^M${rW81>E zovf2joXFbtx>BfAmGy^u8He*$ZI1?@ycY*r{+-}gFFw_`$o0Xo1-I0hXPX*ruI5ks zr+ZRmhW?7pN`|+lY~h~7ll$jgTmF>f=9njyCp{)HJg8t(>R>XDW||?nD*NNR^#5~T zEI9b=`{8|m<DdQ0o28q5-tNIHvxJ~c)eRq~RZey;{+E1=#k<o`IZ-m@@Zy39>kz#p z=1Pk-zHZ5>Tc1?&L|a;NHP<GURb?fq``&t(@mJdO;=+pf?dC_X`OMNcHd%La%Z@&! zX)~k)#QPRiC4QOtE}*?mKIX&uk5AuRe&a0vXYTd#<4sqhlW#EJ3R(8?w_lB{`mXe* zyURn})mJ3_jM>6_!rS<<*!hHl!(klXEY=Acm31Fz=r}E0eqolwox?YuDKc62zxFrZ z^61y~)7o5fYUlg)zh$j^@?r*ejm4(<|MzI@ICuJh=wjQ*hT06h!urbE>+5f4Sk%OP z@7(;U<YwAl<>>QCnK$BNb0srw#7FPWy|iKf#&5Ic?@bQ+z1{ucL!?~;exe3W%ff^@ zWf&M3<QW(kgprFZw0=+v>o;uKXmeo6$8x?-o`TR58Yj<g+^l(0=cMP>%@^A*#4sQ- zEV@OK$QD8RNYu1WyH^jxU-0q)%_1XHti=O<e=#!2F++NPm}e+RfIE|n3=9lg8bM54 z{XB5_#L56ZQ2}j`DZm?H0p`vQWD8F7GoT;YfY|53%D{|ej|Z~hm}AMvhHulxFdXa1 zGAje~i~fHBcszkQ0EcYPOFi87fQRE)8JMvQ#X&rRt0NDw02BdR8kvk4P`dT349uWz zJst;xDs$9MJ;Xdnw7MBHP~OF7Wncz%@d-ErwMBvK2tRAe906}%5OxS=eT(dnoi+?O zYF|(|w|v2uFwhGIVFqNA86A+kh@5X(8JI!k5uUI^H&_zI;4EZ=G0Pvq#$%Q+$nL)B ffD|HV?go`N8180e10_^$25yD`76t|<M-UGHE|so( literal 0 HcmV?d00001 diff --git a/releases/plg_vmshipment_rules_shipping_v2.0.zip b/releases/plg_vmshipment_rules_shipping_v2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..83ec336faa2c9b2e7db00a3b9698b3e5f975ca93 GIT binary patch literal 14616 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%0At2^87 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 K1H*k25Dx%KNNYp@ literal 0 HcmV?d00001 diff --git a/rules_shipping.php b/rules_shipping.php index 6c3265d..f042ae1 100644 --- a/rules_shipping.php +++ b/rules_shipping.php @@ -24,619 +24,20 @@ defined ('_JEXEC') or die('Restricted access'); if (!class_exists ('vmPSPlugin')) { require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); } -if (!class_exists ('plgVmShipmentRules_Shipping')) { -// Only declare the class once... + +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { + require (dirname(__FILE__).DS.'rules_shipping_base.php'); +} /** Shipping costs according to general rules. * Supported Variables: Weight, ZIP, Amount, Products (1 for each product, even if multiple ordered), Articles * Assignable variables: Shipping, Name */ -class plgVmShipmentRules_Shipping extends vmPSPlugin { - - /** - * @param object $subject - * @param array $config - */ +class plgVmShipmentRules_Shipping extends plgVmShipmentRules_Shipping_Base { function __construct (& $subject, $config) { - parent::__construct ($subject, $config); - - $this->_loggable = TRUE; - $this->_tablepkey = 'id'; - $this->_tableId = 'id'; - $this->tableFields = array_keys ($this->getTableSQLFields ()); - $varsToPush = $this->getVarsToPush (); - $this->setConfigParameterable ($this->_configTableFieldName, $varsToPush); - } - - /** - * Create the table for this plugin if it does not yet exist. - * - * @author Valérie Isaksen - */ - public function getVmPluginCreateTableSQL () { - return $this->createTableSQL ('Shipment Rules Table'); - } - - /** - * @return array - */ - function getTableSQLFields () { - $SQLfields = array( - 'id' => 'int(1) UNSIGNED NOT NULL AUTO_INCREMENT', - 'virtuemart_order_id' => 'int(11) UNSIGNED', - 'order_number' => 'char(32)', - 'virtuemart_shipmentmethod_id' => 'mediumint(1) UNSIGNED', - 'shipment_name' => 'varchar(5000)', - 'rule_name' => 'varchar(500)', - 'order_weight' => 'decimal(10,4)', - 'order_articles' => 'int(1)', - 'order_products' => 'int(1)', - 'shipment_weight_unit' => 'char(3) DEFAULT \'KG\'', - 'shipment_cost' => 'decimal(10,2)', - 'tax_id' => 'smallint(1)' - ); - return $SQLfields; - } - - /** - * This method is fired when showing the order details in the frontend. - * It displays the shipment-specific data. - * - * @param integer $virtuemart_order_id The order ID - * @param integer $virtuemart_shipmentmethod_id The selected shipment method id - * @param string $shipment_name Shipment Name - * @return mixed Null for shipments that aren't active, text (HTML) otherwise - * @author Valérie Isaksen - * @author Max Milbers - */ - public function plgVmOnShowOrderFEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id, &$shipment_name) { - $this->onShowOrderFE ($virtuemart_order_id, $virtuemart_shipmentmethod_id, $shipment_name); - } - - /** - * This event is fired after the order has been stored; it gets the shipment method- - * specific data. - * - * @param int $order_id The order_id being processed - * @param object $cart the cart - * @param array $order The actual order saved in the DB - * @return mixed Null when this method was not selected, otherwise true - * @author Valerie Isaksen - */ - function plgVmConfirmedOrder (VirtueMartCart $cart, $order) { - - if (!($method = $this->getVmPluginMethod ($order['details']['BT']->virtuemart_shipmentmethod_id))) { - return NULL; // Another method was selected, do nothing - } - if (!$this->selectedThisElement ($method->shipment_element)) { - return FALSE; - } - $values['virtuemart_order_id'] = $order['details']['BT']->virtuemart_order_id; - $values['order_number'] = $order['details']['BT']->order_number; - $values['virtuemart_shipmentmethod_id'] = $order['details']['BT']->virtuemart_shipmentmethod_id; - $values['shipment_name'] = $this->renderPluginName ($method); - $values['rule_name'] = $method->rule_name; - $values['order_weight'] = $this->getOrderWeight ($cart, $method->weight_unit); - $values['order_articles'] = $this->getOrderArticles ($cart); - $values['order_products'] = $this->getOrderProducts ($cart); - $values['shipment_weight_unit'] = $method->weight_unit; - $values['shipment_cost'] = $method->cost; - $values['tax_id'] = $method->tax_id; - $this->storePSPluginInternalData ($values); - - return TRUE; - } - - /** - * This method is fired when showing the order details in the backend. - * It displays the shipment-specific data. - * NOTE, this plugin should NOT be used to display form fields, since it's called outside - * a form! Use plgVmOnUpdateOrderBE() instead! - * - * @param integer $virtuemart_order_id The order ID - * @param integer $virtuemart_shipmentmethod_id The order shipment method ID - * @param object $_shipInfo Object with the properties 'shipment' and 'name' - * @return mixed Null for shipments that aren't active, text (HTML) otherwise - * @author Valerie Isaksen - */ - public function plgVmOnShowOrderBEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id) { - if (!($this->selectedThisByMethodId ($virtuemart_shipmentmethod_id))) { - return NULL; - } - $html = $this->getOrderShipmentHtml ($virtuemart_order_id); - return $html; - } - - /** - * @param $virtuemart_order_id - * @return string - */ - function getOrderShipmentHtml ($virtuemart_order_id) { - - $db = JFactory::getDBO (); - $q = 'SELECT * FROM `' . $this->_tablename . '` ' - . 'WHERE `virtuemart_order_id` = ' . $virtuemart_order_id; - $db->setQuery ($q); - if (!($shipinfo = $db->loadObject ())) { - vmWarn (500, $q . " " . $db->getErrorMsg ()); - return ''; - } - - if (!class_exists ('CurrencyDisplay')) { - require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'currencydisplay.php'); - } - - $currency = CurrencyDisplay::getInstance (); - $tax = ShopFunctions::getTaxByID ($shipinfo->tax_id); - $taxDisplay = is_array ($tax) ? $tax['calc_value'] . ' ' . $tax['calc_value_mathop'] : $shipinfo->tax_id; - $taxDisplay = ($taxDisplay == -1) ? JText::_ ('COM_VIRTUEMART_PRODUCT_TAX_NONE') : $taxDisplay; - - $html = '<table class="adminlist">' . "\n"; - $html .= $this->getHtmlHeaderBE (); - $html .= $this->getHtmlRowBE ('RULES_SHIPPING_NAME', $shipinfo->shipment_name); - $html .= $this->getHtmlRowBE ('RULES_WEIGHT', $shipinfo->order_weight . ' ' . ShopFunctions::renderWeightUnit ($shipinfo->shipment_weight_unit)); - $html .= $this->getHtmlRowBE ('RULES_ARTICLES', $shipinfo->order_articles . '/' . $shipinfo->order_products); - $html .= $this->getHtmlRowBE ('RULES_COST', $currency->priceDisplay ($shipinfo->shipment_cost)); - $html .= $this->getHtmlRowBE ('RULES_TAX', $taxDisplay); - $html .= '</table>' . "\n"; - - return $html; - } - - /** Include the rule name in the shipment name */ - protected function renderPluginName ($plugin) { - $return = ''; - $plugin_name = $this->_psType . '_name'; - $plugin_desc = $this->_psType . '_desc'; - $description = ''; - // $params = new JParameter($plugin->$plugin_params); - // $logo = $params->get($this->_psType . '_logos'); - $logosFieldName = $this->_psType . '_logos'; - $logos = $plugin->$logosFieldName; - if (!empty($logos)) { - $return = $this->displayLogos ($logos) . ' '; - } - if (!empty($plugin->$plugin_desc)) { - $description = '<span class="' . $this->_type . '_description">' . $plugin->$plugin_desc . '</span>'; - } - $rulename=''; - if (!empty($plugin->rule_name)) { - $rulename=" (".$plugin->rule_name.")"; - } - $pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description; - return $pluginName; - } - - - - /** - * @param VirtueMartCart $cart - * @param $method - * @param $cart_prices - * @return int - */ - function getCosts (VirtueMartCart $cart, $method, $cart_prices) { - if (empty($method->rules)) $this->parseMethodRules($method); - $cartvals = $this->getCartValues ($cart, $cart_prices); - - foreach ($method->rules as $r) { - if ($r->matches($cartvals)) { - $method->tax_id = $r->tax_id; - $method->matched_rule = $r; - $method->rule_name = $r->name; - $method->cost = $r->getShippingCosts($cartvals); - $method->includes_tax = $r->includes_tax; - return $method->cost; - } - } - - vmdebug('getCosts '.$method->name.' does not return shipping costs'); - return 0; - } - - /** - * update the plugin cart_prices ( - * - * @author Valérie Isaksen (original), Reinhold Kainhofer (tax calculations from shippingWithTax) - * - * @param $cart_prices: $cart_prices['salesPricePayment'] and $cart_prices['paymentTax'] updated. Displayed in the cart. - * @param $value : fee - * @param $tax_id : tax id - */ - - function setCartPrices (VirtueMartCart $cart, &$cart_prices, $method) { - - if (!class_exists ('calculationHelper')) { - require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'calculationh.php'); - } - - $calculator = calculationHelper::getInstance (); - - $value = $calculator->roundInternal ($this->getCosts ($cart, $method, $cart_prices), 'salesPrice'); - - $_psType = ucfirst ($this->_psType); - $cart_prices[$this->_psType . 'Value'] = $value; - - $taxrules = array(); - if (!empty($method->tax_id)) { - $cart_prices[$this->_psType . '_calc_id'] = $method->tax_id; - - $db = JFactory::getDBO (); - $q = 'SELECT * FROM #__virtuemart_calcs WHERE `virtuemart_calc_id`="' . $method->tax_id . '" '; - $db->setQuery ($q); - $taxrules = $db->loadAssocList (); - } - - if (count ($taxrules) > 0) { - if ($method->includes_tax) { - $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($cart_prices[$this->_psType . 'Value'], 'salesPrice'); - // Calculate the tax from the final sales price: - $calculator->setRevert (true); - $cart_prices[$this->_psType . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true)); - $calculator->setRevert (false); - } else { - $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value']), 'salesPrice'); - $cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']), 'salesPrice'); - } - $cart_prices[$this->_psType . '_calc_id'] = $taxrules[0]['virtuemart_calc_id']; - } else { - $cart_prices['salesPrice' . $_psType] = $value; - $cart_prices[$this->_psType . 'Tax'] = 0; - $cart_prices[$this->_psType . '_calc_id'] = 0; - } - } - - private function parseMethodRule ($rulestring, $countries, $tax, &$method) { - $rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring); - foreach ($rules1 as $r) { - // Ignore empty lines - if (empty($r)) continue; - if (class_exists('ShippingRule_Advanced')) { - $method->rules[]=new ShippingRule_Advanced($r, $countries, $tax); - } else { - $method->rules[]=new ShippingRule($r, $countries, $tax); - } - } - } - - protected function parseMethodRules (&$method) { - $this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method); - $this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method); - $this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method); - $this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method); - $this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method); - $this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method); - $this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method); - $this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method); - } - - protected function getOrderArticles (VirtueMartCart $cart) { - /* Cache the value in a static variable and calculate it only once! */ - static $articles = 0; - if(empty($articles) and count($cart->products)>0){ - foreach ($cart->products as $product) { - $articles += $product->quantity; - } - } - return $articles; - } - - protected function getOrderDimensions (VirtueMartCart $cart) { - /* Cache the value in a static variable and calculate it only once! */ - static $calculated = 0; - static $dimensions=array( - 'volume' => 0, - 'maxvolume' => 0, 'minvolume' => 9999999999, - 'maxlength' => 0, 'minlength' => 9999999999, - 'maxwidth' => 0, 'minwidth' => 9999999999, - 'maxheight' => 0, 'minheight' => 9999999999, - ); - if ($calculated==0) { - $calculated=1; - foreach ($cart->products as $product) { - $volume = $product->product_length * $product->product_width * $product->product_height; - $dimensions['volume'] += $volume * $product->quantity; - $dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume); - $dimensions['minvolume'] = min ($dimensions['minvolume'], $volume); - $dimensions['maxlength'] = max ($dimensions['maxlength'], $product->product_length); - $dimensions['minlength'] = min ($dimensions['minlength'], $product->product_length); - $dimensions['maxwidth'] = max ($dimensions['maxwidth'], $product->product_width); - $dimensions['minwidth'] = min ($dimensions['minwidth'], $product->product_width); - $dimensions['maxheight'] = max ($dimensions['maxheight'], $product->product_height); - $dimensions['minheight'] = min ($dimensions['minheight'], $product->product_height); - $articles += $product->quantity; - } - } - return $dimensions; - } - - protected function getOrderProducts (VirtueMartCart $cart) { - /* Cache the value in a static variable and calculate it only once! */ - static $products = 0; - if(empty($products) and count($cart->products)>0){ - $products = count($cart->products); - } - return $products; - } - - protected function getCartValues (VirtueMartCart $cart, $cart_prices) { - $orderWeight = $this->getOrderWeight ($cart, $method->weight_unit); - $dimensions = $this->getOrderDimensions ($cart); - $address = (($cart->ST == 0) ? $cart->BT : $cart->ST); - - $products = 0; - $articles = 0; - foreach ($cart->products as $product) { - $products += 1; - $articles += $product->quantity; - } - $cartvals = array('weight'=>$orderWeight, - 'zip'=>$address['zip'], - 'articles'=>$articles, - 'products'=>$products, - 'amount'=>$cart_prices['salesPrice'], - 'country'=>$address['virtuemart_country_id'], - 'volume' => $dimensions['volume'], - 'maxvolume' => $dimensions['maxvolume'], - 'minvolume' => $dimensions['minvolume'], - 'maxlength' => $dimensions['maxlength'], - 'minlength' => $dimensions['minlength'], - 'maxwidth' => $dimensions['maxwidth'], - 'minwidth' => $dimensions['minwidth'], - 'maxheight' => $dimensions['maxheight'], - 'minheight' => $dimensions['minheight'] - ); - return $cartvals; - } - - - /** - * @param \VirtueMartCart $cart - * @param int $method - * @param array $cart_prices - * @return bool - */ - protected function checkConditions ($cart, $method, $cart_prices) { - if (empty($method->rules)) $this->parseMethodRules($method); - - $cartvals = $this->getCartValues ($cart, $cart_prices); - foreach ($method->rules as $r) { - if ($r->matches($cartvals)) { - $method->matched_rule = $r; - $method->rule_name = $r->name; - return TRUE; - } - } - vmdebug('checkConditions '.$method->name.' does not fit'); - return FALSE; - } - - /** - * Create the table for this plugin if it does not yet exist. - * This functions checks if the called plugin is active one. - * When yes it is calling the standard method to create the tables - * - * @author Valérie Isaksen - * - */ - function plgVmOnStoreInstallShipmentPluginTable ($jplugin_id) { - return $this->onStoreInstallPluginTable ($jplugin_id); - } - - /** - * @param VirtueMartCart $cart - * @return null - */ - public function plgVmOnSelectCheckShipment (VirtueMartCart &$cart) { - return $this->OnSelectCheck ($cart); - } - - /** - * plgVmDisplayListFE - * This event is fired to display the pluginmethods in the cart (edit shipment/payment) for example - * - * @param object $cart Cart object - * @param integer $selected ID of the method selected - * @return boolean True on success, false on failures, null when this plugin was not selected. - * On errors, JError::raiseWarning (or JError::raiseError) must be used to set a message. - * - * @author Valerie Isaksen - * @author Max Milbers - */ - public function plgVmDisplayListFEShipment (VirtueMartCart $cart, $selected = 0, &$htmlIn) { - return $this->displayListFE ($cart, $selected, $htmlIn); - } - - /** - * @param VirtueMartCart $cart - * @param array $cart_prices - * @param $cart_prices_name - * @return bool|null - */ - public function plgVmOnSelectedCalculatePriceShipment (VirtueMartCart $cart, array &$cart_prices, &$cart_prices_name) { - return $this->onSelectedCalculatePrice ($cart, $cart_prices, $cart_prices_name); - } - - /** - * plgVmOnCheckAutomaticSelected - * Checks how many plugins are available. If only one, the user will not have the choice. Enter edit_xxx page - * The plugin must check first if it is the correct type - * - * @author Valerie Isaksen - * @param VirtueMartCart cart: the cart object - * @return null if no plugin was found, 0 if more then one plugin was found, virtuemart_xxx_id if only one plugin is found - * - */ - function plgVmOnCheckAutomaticSelectedShipment (VirtueMartCart $cart, array $cart_prices = array(), &$shipCounter) { - if ($shipCounter > 1) { - return 0; - } - return $this->onCheckAutomaticSelected ($cart, $cart_prices, $shipCounter); - } - - /** - * This method is fired when showing when priting an Order - * It displays the the payment method-specific data. - * - * @param integer $_virtuemart_order_id The order ID - * @param integer $method_id method used for this order - * @return mixed Null when for payment methods that were not selected, text (HTML) otherwise - * @author Valerie Isaksen - */ - function plgVmonShowOrderPrint ($order_number, $method_id) { - return $this->onShowOrderPrint ($order_number, $method_id); - } - - function plgVmDeclarePluginParamsShipment ($name, $id, &$data) { - return $this->declarePluginParams ('shipment', $name, $id, $data); - } - - - /** - * @author Max Milbers - * @param $data - * @param $table - * @return bool - */ - function plgVmSetOnTablePluginShipment(&$data,&$table){ - - $name = $data['shipment_element']; - $id = $data['shipment_jplugin_id']; - - if (!empty($this->_psType) and !$this->selectedThis ($this->_psType, $name, $id)) { - return FALSE; - } else { - // Try to parse all rules (and spit out error) to inform the user: - $method = new StdClass (); - $this->parseMethodRule ($data['rules1'], $data['countries1'], $data['tax_id1'], $method); - $this->parseMethodRule ($data['rules2'], $data['countries2'], $data['tax_id2'], $method); - $this->parseMethodRule ($data['rules3'], $data['countries3'], $data['tax_id3'], $method); - $this->parseMethodRule ($data['rules4'], $data['countries4'], $data['tax_id4'], $method); - $this->parseMethodRule ($data['rules5'], $data['countries5'], $data['tax_id5'], $method); - $this->parseMethodRule ($data['rules6'], $data['countries6'], $data['tax_id6'], $method); - $this->parseMethodRule ($data['rules7'], $data['countries7'], $data['tax_id7'], $method); - $this->parseMethodRule ($data['rules8'], $data['countries8'], $data['tax_id8'], $method); - return $this->setOnTablePluginParams ($name, $id, $table); - } - } - - -} - -class ShippingRule { - var $rulestring = ''; - var $countries = array(); - var $tax_id = 0; - var $conditions = array(); - var $shipping = 0; - var $includes_tax = 0; - var $name = ''; - - function __construct ($rule, $countries, $tax_id) { - if (is_array($countries)) { - $this->countries = $countries; - } elseif (!empty($countries)) { - $this->countries[0] = $countries; - } - $this->tax_id = $tax_id; - $this->rulestring = $rule; - $this->parseRule($rule); - } - - function handleAssignment ($variable, $value, $rulepart) { - switch ($variable) { - case 'shipping': $this->shipping = $this->parseShippingTerm($value); break; - case 'shippingwithtax': $this->shipping = $this->parseShippingTerm($value); $this->includes_tax = True; break; - case 'name': $this->name = $value; break; - default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $variable, $rulepart), 'error'); - } - } - function parseRule($rule) { - $ruleparts=explode(';', $rule); - $operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '=='); - $op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/'; - foreach ($ruleparts as $p) { - $p = trim($p); - if (empty($p)) continue; - $atoms = preg_split ($op_re, $p, -1, PREG_SPLIT_DELIM_CAPTURE); - if (count($atoms)==1) { - $this->shipping = $this->parseShippingTerm($atoms[0]); - } elseif ($atoms[1]=='=') { - $this->handleAssignment (strtolower($atoms[0]), $atoms[2], $p); - } else { - // Conditions, need at least three atoms! - while (count($atoms)>1) { - if (in_array ($atoms[1], $operators)) { - $this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2])); - array_shift($atoms); - array_shift($atoms); - } else { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $p), 'error'); - $atoms = array(); - } - } - } - } - } - - function parseShippingTerm($expr) { - /* In the advanced version, shipping cost can be given as a full mathematical expression */ - return strtolower($expr); - } - - function evaluateTerm ($expr, $vals) { - /* In the advanced version, all conditions and costs can be given as a full mathematical expression */ - $e=$expr; - // Strings indicate members of the $vals array (weight, amount, etc.) - if (!is_numeric($e) && isset($vals[$e])) $e=$vals[$e]; - if (!is_numeric($e)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error'); - $e = 0; - } - return $e; - } - - function calculateShipping ($vals) { - return $this->evaluateTerm($this->shipping, $vals); - } - - function matches($vals) { - // First, check the country, if any conditions are given: - if (count ($this->countries) > 0 && !in_array ($vals['country'], $this->countries)) - return False; - - foreach ($this->conditions as $c) { - $v1=$this->evaluateTerm($c[1], $vals); - $v2=$this->evaluateTerm($c[2], $vals); - // Unknown strings / unevaluatable terms cause the rule to NOT match: - if (!is_numeric($v1) || !is_numeric($v2)) { - return false; - } - $res = false; - switch ($c[0]) { - case '<': $res = ($v1<$v2); break; - case '<=': - case '=<': $res = ($v1<=$v2); break; - case '==': $res = ($v1==$v2); break; - case '!=': - case '<>': $res = ($v1!=$v2); break; - case '>=': - case '=>': $res = ($v1>=$v2); break; - case '>': $res = ($v1>$v2); break; - } - // Immediately return, if this rule does not match - if (!$res) return false; - } - // All conditions match, so return true - return true; - } - - function getShippingCosts($vals) { - return $this->calculateShipping($vals); } } -} // No closing tag diff --git a/rules_shipping.xml b/rules_shipping.xml index a1805d4..3c30b28 100644 --- a/rules_shipping.xml +++ b/rules_shipping.xml @@ -1,15 +1,16 @@ <?xml version="1.0" encoding="UTF-8" ?> <install version="1.5" type="plugin" group="vmshipment" method="upgrade"> <name>VMSHIPMENT_RULES</name> - <creationDate>2013-01-12</creationDate> + <creationDate>2013-02-08</creationDate> <author>Reinhold Kainhofer</author> <authorUrl>http://www.kainhofer.com</authorUrl> <copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright> <license>GPL v3+</license> - <version>1.1.0</version> + <version>2.0.0</version> <description>VMSHIPMENT_RULES_DESC</description> <files> <filename plugin="rules_shipping">rules_shipping.php</filename> + <filename>rules_shipping_base.php</filename> <folder>language</folder> <folder>elements</folder> </files> diff --git a/rules_shipping_advanced.php b/rules_shipping_advanced.php index d651d1a..92b92bc 100644 --- a/rules_shipping_advanced.php +++ b/rules_shipping_advanced.php @@ -25,28 +25,98 @@ defined ('_JEXEC') or die('Restricted access'); if (!class_exists ('vmPSPlugin')) { require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); } -if (!class_exists ('plgVmShipmentRules_Shipping')) { - require (dirname(__FILE__).DS.'rules_shipping.php'); +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { + require (dirname(__FILE__).DS.'rules_shipping_base.php'); } /** Shipping costs according to general rules. * Derived from the standard plugin, no need to change anything! The standard plugin already uses the advanced rules class defined below, if it can be found */ -class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping { -// function __construct (& $subject, $config) { -// parent::__construct ($subject, $config); -// } +class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_Base { + function __construct (& $subject, $config) { + parent::__construct ($subject, $config); + } + protected function createMethodRule ($r, $countries, $tax) { + return new ShippingRule_Advanced ($r, $countries, $tax); + } + /** Allow child classes to add additional variables for the rules + */ + protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) { + $values['coupon'] = $cart->couponCode; + } + +} + +// is_comparison: if the expression $o is a (possibly chained) comparison consisting of operators in $ops, +// return the last term of hte comparison; null otherwise +function is_comparison ($o, $ops) { + if (!is_array($o)) + return null; + if (in_array($o[0], $ops)) + return $o[2]; + $o2comp = is_comparison($o[2], $ops); + if (trim($o[0])=='AND' && !is_null (is_comparison($o[1], $ops)) && !is_null($o2comp)) + return $o2comp; + return null; } - + + /** Extend the shipping rules by allowing arbitrary mathematical expressions */ class ShippingRule_Advanced extends ShippingRule { - var $operators = array("+"=>10, "-"=>10, "*"=>20, "/"=>20, "%"=>30, "^"=>50, "("=>100, ")"=>100); + var $operators = array( + "^" => 70, + "*" => 60, "/" => 60, "%" => 60, + "+" => 50, "-" => 50, + "<" => 40, "<=" => 40, ">" => 40, ">=" => 40, "=>" => 40, "=<" => 40, + "==" => 40, "!=" => 40, "<>" => 40, + "&" => 21, + " OR " => 20, + "=" => 10, + + "(" => 0, ")" =>0 ); function __construct ($rule, $countries, $tax_id) { parent::__construct ($rule, $countries, $tax_id); } + + function tokenize_expression ($expression) { + // First, extract all strings, delimited by ": + $str_re = '/("(?:\\"|[^"])*"|\'(?:\\\'|[^\'])*\')/'; + $strings = preg_split($str_re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + // Then split all other parts of the expression at the operators + $op_re = ':\s*( OR |<=|=>|>=|=>|<>|!=|==|<|=|>|\+|-|\*|/|%|\(|\)|\^)\s*:'; + $atoms = array(); + foreach ($strings as $s) { + if (preg_match($str_re, $s)) { + $atoms[] = $s; + } else { + $newatoms = preg_split($op_re, $s, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + $atoms = array_merge ($atoms, $newatoms); + } + } + // Finally handle the double meaning of -: After an operator and before an operand, it is NOT an operator, but belongs to the operand + $prevop = true; + $result = array(); + $leftover = ''; + foreach ($atoms as $a) { + if ($a == "-") { + $prev = end($result); + if (is_null ($prev) || (preg_match ($op_re, $prev) && $prev != ')')) { + $leftover = $a; + } else { + $result[] = $leftover.$a; + $leftover = ''; + } + } else { + $result[] = $leftover.$a; + $leftover = ''; + } + } + return $result; + } + /** parse the mathematical expressions (following Knuth 1962): * First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators) @@ -63,50 +133,74 @@ class ShippingRule_Advanced extends ShippingRule { * 6a) Pop operators from stack until opening parenthesis is found * 6b) push them to the result (not the opening parenthesis, of course) * 7) At the end of the input, pop all operators from the stack and onto the result - * Store this RPN list for later evaluation + * + * Afterwards, convert this RPN list into an expression tree to be evaluate + * */ - function parseShippingTerm ($expr) { - $op_re = ':\s*(\+|-|\*|/|%|\(|\)|\^)\s*:'; - $atoms = preg_split($op_re, strtolower($expr), -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + function parseRulePart($rulepart) { + /* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */ + /* In the advanced version, all conditions and costs can be given as a full mathematical expression */ + /* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */ + $rulepart = trim($rulepart); + if (empty($rulepart)) return; + + + // Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression! + if (preg_match('/^\s*(name)\s*=\s*"?(.*)"?\s*$/i', $rulepart, $matches)) { + $this->handleAssignment ($matches[1], $matches[2], $rulepart); + return; + } + + // Split at all operators: + $atoms = $this->tokenize_expression ($rulepart); + + // Any of these indicate a comparison and thus a condition: + $comparison_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>'); + $is_comparison = false; + $is_assignment = false; + if (count($atoms)==1) return $atoms[0]; - // Operators,including precedence $stack = array (); // 1) - $result = array (); + $rpn = array (); foreach ($atoms as $a) { // 2) - if (!isset($this->operators[$a])) { // 3) - array_push ($result, $a); - } elseif ($a == "(") { // 5) + if (!isset($this->operators[$a])) { // 3) Operand + array_push ($rpn, $a); + } elseif ($a == "(") { // 5) parenthesis array_push ($stack, $a); - } elseif ($a == ")") { // 6) + } elseif ($a == ")") { // 6) parenthesis do { $op=array_pop($stack); // 6a) - if (!empty($op) && $op != "(") { - array_push ($result, $op); // 6b) + if (!is_null($op) && ($op != "(")) { + array_push ($rpn, $op); // 6b) } else { if ($op != "(") { // If no ( can be found, the expression is wrong! - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $expr), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error'); } break; // We have found the opening parenthesis } - } while (0); - } else { // 4 + } while (true); + } else { // 4) operators // For operators, pop operators from the stack until you reach an opening parenthesis, // an operator of lower precedence, or a right associative symbol of equal precedence. $prec = $this->operators[$a]; + $is_comparison |= in_array($a, $comparison_ops); + $is_assignment |= ($a == "="); while (count($stack)>0) { // 4a) $op = array_pop ($stack); - // Ignore the right-associative symbols of equal precedence for now... + // The only right-associative operator is =, which we allow at most once! if ($op == "(") { + // add it back to the stack! + array_push ($stack, $op); break; } elseif ($this->operators[$op]<$prec) { // We found an operator with lower precedence, add it back to the stack! array_push ($stack, $op); // 4b) break; } else { - array_push ($result, $op); + array_push ($rpn, $op); } } while (0); array_push ($stack, $a); // 4b) @@ -116,27 +210,13 @@ class ShippingRule_Advanced extends ShippingRule { while ($op=array_pop($stack)) { // Opening parentheses should not be found on the stack any more. That would mean a closing paren is missing! if ($op == "(") { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $expr), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error'); } else { - array_push ($result, $op); + array_push ($rpn, $op); } } - /* In the advanced version, shipping cost can be given as a full mathematical expression */ - return $result; - } - function evaluateTerm ($expr, $vals) { - if (!is_array($expr)) { - // Normal expression, no complex mathematical formula in RPN - if (!is_numeric($expr) && isset($vals[$expr])) $expr=$vals[$expr]; - if (!is_numeric($expr)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $expr, $this->rulestring), 'error'); - return 0; - } - return $expr; - } - - /** Evaluate the RPN, according to Knuth: + /** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure, according to Knuth: * 1) Initialize an empty stack * 2) Read the RPN from left to right * 3) If operand, push it onto the stack @@ -148,49 +228,58 @@ class ShippingRule_Advanced extends ShippingRule { * 5) At the end of the RPN, pop the result from the stack. * 5a) The stack should now be empty (otherwise, ERROR, invalid syntax) */ + $stack=array(); // 1) - foreach ($expr as $e) { // 2) + foreach ($rpn as $e) { // 2) if (!isset($this->operators[$e])) { // 3) // Operand => push onto stack - if (!is_numeric($e) && isset($vals[$e])) $e = $vals[$e]; - if (!is_numeric($e)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error'); - $e = 0; - } array_push ($stack, $e); } else { // 4) // Operator => apply to the last two values on the stack if (count($stack)<2) { // 4d) - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $this->rulestring), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error'); array_push($stack, 0); continue; } $o2 = array_pop($stack); // 4a) $o1 = array_pop($stack); - $res = 0; - switch ($e) { // 4b) - case "+": $res = $o1+$o2; break; - case "-": $res = $o1-$o2; break; - case "*": $res = $o1*$o2; break; - case "/": $res = $o1/$o2; break; - case "%": $res = $o1%$o2; break; - case "^": $res = $o1^$o1; break; - default: - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR', $e, $this->rulestring), 'error'); - $res = 0; + // TODO: Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison, + // insert AND(o1, e(arg2(o1), o2)) instead of e(o1, o1) + // is_comparison nicely returns the last term of the (possibly chained) comparison, null otherwise + $o1comp = is_comparison($o1, $comparison_ops); + if (in_array ($e, $comparison_ops) && !is_null($o1comp) ) { + $op = array ('AND', $o1, array($e, $o1comp, $o2)); + } else { + $op = array ($e, $o1, $o2); // 4b) } - array_push($stack, $res); // 4c) + array_push ($stack, $op); // 4c) } } // 5a) if (count($stack) != 1) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $rulepart), 'error'); $stack = array (0); } $res = array_pop($stack); // 5) - return $res; + + if ($is_comparison) { // Comparisons are conditions + $this->conditions[] = $res; + } elseif ($is_assignment) { + + if ($res[0]=='=') { + $this->handleAssignment ($res[1], $res[2], $rulepart); + } else { + // Assignment has to be top-level! + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL', $rulepart), 'error'); + } + } else { + // Terms without comparisons or assignments are shipping cost expressions + $this->shipping = $res; + $this->includes_tax = False; + } } + } // No closing tag diff --git a/rules_shipping_advanced.xml b/rules_shipping_advanced.xml index 71fe475..278d8ed 100644 --- a/rules_shipping_advanced.xml +++ b/rules_shipping_advanced.xml @@ -1,16 +1,16 @@ <?xml version="1.0" encoding="UTF-8" ?> <install version="1.5" type="plugin" group="vmshipment" method="upgrade"> <name>VMSHIPMENT_RULES_ADV</name> - <creationDate>2013-01-16</creationDate> + <creationDate>2013-02-08</creationDate> <author>Reinhold Kainhofer</author> <authorUrl>http://www.kainhofer.com</authorUrl> <copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright> <license>GPL v3+</license> - <version>1.1.0</version> + <version>2.0.0</version> <description>VMSHIPMENT_RULES_ADV_DESC</description> <files> <filename plugin="rules_shipping_advanced">rules_shipping_advanced.php</filename> - <filename>rules_shipping.php</filename> + <filename>rules_shipping_base.php</filename> <folder>language</folder> <folder>elements</folder> </files> diff --git a/rules_shipping_base.php b/rules_shipping_base.php new file mode 100644 index 0000000..c42290d --- /dev/null +++ b/rules_shipping_base.php @@ -0,0 +1,694 @@ +<?php + +defined ('_JEXEC') or die('Restricted access'); + +/** + * Shipment plugin for general, rules-based shipments, like regular postal services with complex shipping cost structures + * + * @version $Id$ + * @package VirtueMart + * @subpackage Plugins - shipment + * @copyright Copyright (C) 2004-2012 VirtueMart Team - All rights reserved. + * @copyright Copyright (C) 2013 Reinhold Kainhofer, reinhold@kainhofer.com + * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.php + * VirtueMart is free software. This version may have been modified pursuant + * to the GNU General Public License, and as distributed it includes or + * is derivative of works licensed under the GNU General Public License or + * other free or open source software licenses. + * See /administrator/components/com_virtuemart/COPYRIGHT.php for copyright notices and details. + * + * http://virtuemart.org + * @author Reinhold Kainhofer, based on the weight_countries shipping plugin by Valerie Isaksen + * + */ +if (!class_exists ('vmPSPlugin')) { + require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); +} +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { +// Only declare the class once... + +/** Shipping costs according to general rules. + * Supported Variables: Weight, ZIP, Amount, Products (1 for each product, even if multiple ordered), Articles + * Assignable variables: Shipping, Name + */ +class plgVmShipmentRules_Shipping_Base extends vmPSPlugin { + + /** + * @param object $subject + * @param array $config + */ + function __construct (& $subject, $config) { + parent::__construct ($subject, $config); + + $this->_loggable = TRUE; + $this->_tablepkey = 'id'; + $this->_tableId = 'id'; + $this->tableFields = array_keys ($this->getTableSQLFields ()); + $varsToPush = $this->getVarsToPush (); + $this->setConfigParameterable ($this->_configTableFieldName, $varsToPush); + } + + /** + * Create the table for this plugin if it does not yet exist. + * + * @author Valérie Isaksen + */ + public function getVmPluginCreateTableSQL () { + return $this->createTableSQL ('Shipment Rules Table'); + } + + /** + * @return array + */ + function getTableSQLFields () { + $SQLfields = array( + 'id' => 'int(1) UNSIGNED NOT NULL AUTO_INCREMENT', + 'virtuemart_order_id' => 'int(11) UNSIGNED', + 'order_number' => 'char(32)', + 'virtuemart_shipmentmethod_id' => 'mediumint(1) UNSIGNED', + 'shipment_name' => 'varchar(5000)', + 'rule_name' => 'varchar(500)', + 'order_weight' => 'decimal(10,4)', + 'order_articles' => 'int(1)', + 'order_products' => 'int(1)', + 'shipment_weight_unit' => 'char(3) DEFAULT \'KG\'', + 'shipment_cost' => 'decimal(10,2)', + 'tax_id' => 'smallint(1)' + ); + return $SQLfields; + } + + /** + * This method is fired when showing the order details in the frontend. + * It displays the shipment-specific data. + * + * @param integer $virtuemart_order_id The order ID + * @param integer $virtuemart_shipmentmethod_id The selected shipment method id + * @param string $shipment_name Shipment Name + * @return mixed Null for shipments that aren't active, text (HTML) otherwise + * @author Valérie Isaksen + * @author Max Milbers + */ + public function plgVmOnShowOrderFEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id, &$shipment_name) { + $this->onShowOrderFE ($virtuemart_order_id, $virtuemart_shipmentmethod_id, $shipment_name); + } + + /** + * This event is fired after the order has been stored; it gets the shipment method- + * specific data. + * + * @param int $order_id The order_id being processed + * @param object $cart the cart + * @param array $order The actual order saved in the DB + * @return mixed Null when this method was not selected, otherwise true + * @author Valerie Isaksen + */ + function plgVmConfirmedOrder (VirtueMartCart $cart, $order) { + + if (!($method = $this->getVmPluginMethod ($order['details']['BT']->virtuemart_shipmentmethod_id))) { + return NULL; // Another method was selected, do nothing + } + if (!$this->selectedThisElement ($method->shipment_element)) { + return FALSE; + } + $values['virtuemart_order_id'] = $order['details']['BT']->virtuemart_order_id; + $values['order_number'] = $order['details']['BT']->order_number; + $values['virtuemart_shipmentmethod_id'] = $order['details']['BT']->virtuemart_shipmentmethod_id; + $values['shipment_name'] = $this->renderPluginName ($method); + $values['rule_name'] = $method->rule_name; + $values['order_weight'] = $this->getOrderWeight ($cart, $method->weight_unit); + $values['order_articles'] = $this->getOrderArticles ($cart); + $values['order_products'] = $this->getOrderProducts ($cart); + $values['shipment_weight_unit'] = $method->weight_unit; + $values['shipment_cost'] = $method->cost; + $values['tax_id'] = $method->tax_id; + $this->storePSPluginInternalData ($values); + + return TRUE; + } + + /** + * This method is fired when showing the order details in the backend. + * It displays the shipment-specific data. + * NOTE, this plugin should NOT be used to display form fields, since it's called outside + * a form! Use plgVmOnUpdateOrderBE() instead! + * + * @param integer $virtuemart_order_id The order ID + * @param integer $virtuemart_shipmentmethod_id The order shipment method ID + * @param object $_shipInfo Object with the properties 'shipment' and 'name' + * @return mixed Null for shipments that aren't active, text (HTML) otherwise + * @author Valerie Isaksen + */ + public function plgVmOnShowOrderBEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id) { + if (!($this->selectedThisByMethodId ($virtuemart_shipmentmethod_id))) { + return NULL; + } + $html = $this->getOrderShipmentHtml ($virtuemart_order_id); + return $html; + } + + /** + * @param $virtuemart_order_id + * @return string + */ + function getOrderShipmentHtml ($virtuemart_order_id) { + + $db = JFactory::getDBO (); + $q = 'SELECT * FROM `' . $this->_tablename . '` ' + . 'WHERE `virtuemart_order_id` = ' . $virtuemart_order_id; + $db->setQuery ($q); + if (!($shipinfo = $db->loadObject ())) { + vmWarn (500, $q . " " . $db->getErrorMsg ()); + return ''; + } + + if (!class_exists ('CurrencyDisplay')) { + require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'currencydisplay.php'); + } + + $currency = CurrencyDisplay::getInstance (); + $tax = ShopFunctions::getTaxByID ($shipinfo->tax_id); + $taxDisplay = is_array ($tax) ? $tax['calc_value'] . ' ' . $tax['calc_value_mathop'] : $shipinfo->tax_id; + $taxDisplay = ($taxDisplay == -1) ? JText::_ ('COM_VIRTUEMART_PRODUCT_TAX_NONE') : $taxDisplay; + + $html = '<table class="adminlist">' . "\n"; + $html .= $this->getHtmlHeaderBE (); + $html .= $this->getHtmlRowBE ('RULES_SHIPPING_NAME', $shipinfo->shipment_name); + $html .= $this->getHtmlRowBE ('RULES_WEIGHT', $shipinfo->order_weight . ' ' . ShopFunctions::renderWeightUnit ($shipinfo->shipment_weight_unit)); + $html .= $this->getHtmlRowBE ('RULES_ARTICLES', $shipinfo->order_articles . '/' . $shipinfo->order_products); + $html .= $this->getHtmlRowBE ('RULES_COST', $currency->priceDisplay ($shipinfo->shipment_cost)); + $html .= $this->getHtmlRowBE ('RULES_TAX', $taxDisplay); + $html .= '</table>' . "\n"; + + return $html; + } + + /** Include the rule name in the shipment name */ + protected function renderPluginName ($plugin) { + $return = ''; + $plugin_name = $this->_psType . '_name'; + $plugin_desc = $this->_psType . '_desc'; + $description = ''; + // $params = new JParameter($plugin->$plugin_params); + // $logo = $params->get($this->_psType . '_logos'); + $logosFieldName = $this->_psType . '_logos'; + $logos = $plugin->$logosFieldName; + if (!empty($logos)) { + $return = $this->displayLogos ($logos) . ' '; + } + if (!empty($plugin->$plugin_desc)) { + $description = '<span class="' . $this->_type . '_description">' . $plugin->$plugin_desc . '</span>'; + } + $rulename=''; + if (!empty($plugin->rule_name)) { + $rulename=" (".$plugin->rule_name.")"; + } + $pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description; + return $pluginName; + } + + + + /** + * @param VirtueMartCart $cart + * @param $method + * @param $cart_prices + * @return int + */ + function getCosts (VirtueMartCart $cart, $method, $cart_prices) { + if (empty($method->rules)) $this->parseMethodRules($method); + $cartvals = $this->getCartValues ($cart, $method, $cart_prices); + + foreach ($method->rules as $r) { + if ($r->matches($cartvals)) { + $method->tax_id = $r->tax_id; + $method->matched_rule = $r; + $method->rule_name = $r->name; + $method->cost = $r->getShippingCosts($cartvals); + $method->includes_tax = $r->includes_tax; + return $method->cost; + } + } + + vmdebug('getCosts '.$method->name.' does not return shipping costs'); + return 0; + } + + /** + * update the plugin cart_prices ( + * + * @author Valérie Isaksen (original), Reinhold Kainhofer (tax calculations from shippingWithTax) + * + * @param $cart_prices: $cart_prices['salesPricePayment'] and $cart_prices['paymentTax'] updated. Displayed in the cart. + * @param $value : fee + * @param $tax_id : tax id + */ + + function setCartPrices (VirtueMartCart $cart, &$cart_prices, $method) { + + if (!class_exists ('calculationHelper')) { + require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'calculationh.php'); + } + + $calculator = calculationHelper::getInstance (); + + $value = $calculator->roundInternal ($this->getCosts ($cart, $method, $cart_prices), 'salesPrice'); + + $_psType = ucfirst ($this->_psType); + $cart_prices[$this->_psType . 'Value'] = $value; + + $taxrules = array(); + if (!empty($method->tax_id)) { + $cart_prices[$this->_psType . '_calc_id'] = $method->tax_id; + + $db = JFactory::getDBO (); + $q = 'SELECT * FROM #__virtuemart_calcs WHERE `virtuemart_calc_id`="' . $method->tax_id . '" '; + $db->setQuery ($q); + $taxrules = $db->loadAssocList (); + } + + if (count ($taxrules) > 0) { + if ($method->includes_tax) { + $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($cart_prices[$this->_psType . 'Value'], 'salesPrice'); + // Calculate the tax from the final sales price: + $calculator->setRevert (true); + $cart_prices[$this->_psType . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true)); + $calculator->setRevert (false); + } else { + $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value']), 'salesPrice'); + $cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']), 'salesPrice'); + } + $cart_prices[$this->_psType . '_calc_id'] = $taxrules[0]['virtuemart_calc_id']; + } else { + $cart_prices['salesPrice' . $_psType] = $value; + $cart_prices[$this->_psType . 'Tax'] = 0; + $cart_prices[$this->_psType . '_calc_id'] = 0; + } + } + protected function createMethodRule ($r, $countries, $tax) { + return new ShippingRule($r, $countries, $tax); + } + + private function parseMethodRule ($rulestring, $countries, $tax, &$method) { + $rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring); + foreach ($rules1 as $r) { + // Ignore empty lines + if (empty($r)) continue; + $method->rules[] = $this->createMethodRule ($r, $countries, $tax); + } + } + + protected function parseMethodRules (&$method) { + $this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method); + $this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method); + $this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method); + $this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method); + $this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method); + $this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method); + $this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method); + $this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method); + } + + protected function getOrderArticles (VirtueMartCart $cart) { + /* Cache the value in a static variable and calculate it only once! */ + static $articles = 0; + if(empty($articles) and count($cart->products)>0){ + foreach ($cart->products as $product) { + $articles += $product->quantity; + } + } + return $articles; + } + + protected function getOrderDimensions (VirtueMartCart $cart) { + /* Cache the value in a static variable and calculate it only once! */ + static $calculated = 0; + static $dimensions=array( + 'volume' => 0, + 'maxvolume' => 0, 'minvolume' => 9999999999, + 'maxlength' => 0, 'minlength' => 9999999999, + 'maxwidth' => 0, 'minwidth' => 9999999999, + 'maxheight' => 0, 'minheight' => 9999999999, + ); + if ($calculated==0) { + $calculated=1; + foreach ($cart->products as $product) { + $volume = $product->product_length * $product->product_width * $product->product_height; + $dimensions['volume'] += $volume * $product->quantity; + $dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume); + $dimensions['minvolume'] = min ($dimensions['minvolume'], $volume); + $dimensions['maxlength'] = max ($dimensions['maxlength'], $product->product_length); + $dimensions['minlength'] = min ($dimensions['minlength'], $product->product_length); + $dimensions['maxwidth'] = max ($dimensions['maxwidth'], $product->product_width); + $dimensions['minwidth'] = min ($dimensions['minwidth'], $product->product_width); + $dimensions['maxheight'] = max ($dimensions['maxheight'], $product->product_height); + $dimensions['minheight'] = min ($dimensions['minheight'], $product->product_height); + } + } + return $dimensions; + } + + protected function getOrderProducts (VirtueMartCart $cart) { + /* Cache the value in a static variable and calculate it only once! */ + static $products = 0; + if(empty($products) and count($cart->products)>0){ + $products = count($cart->products); + } + return $products; + } + + /** Allow child classes to add additional variables for the rules + */ + protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) { + } + protected function getCartValues (VirtueMartCart $cart, $method, $cart_prices) { + $orderWeight = $this->getOrderWeight ($cart, $method->weight_unit); + $address = (($cart->ST == 0) ? $cart->BT : $cart->ST); + + $products = 0; + $articles = 0; + foreach ($cart->products as $product) { + $products += 1; + $articles += $product->quantity; + } + $cartvals = array('weight'=>$orderWeight, + 'zip'=>$address['zip'], + 'articles'=>$articles, + 'products'=>$products, + 'amount'=>$cart_prices['salesPrice'], + 'amountWithTax'=>$cart_prices['salesPrice'], + 'amountWithoutTax'=>$cart_prices['priceWithoutTax'], + + 'basePrice'=>$cart_prices['basePrice'], + 'basePriceWithTax'=>$cart_prices['basePriceWithTax'], + 'discountedPriceWithoutTax'=>$cart_prices['discountedPriceWithoutTax'], + 'salesPrice'=>$cart_prices['salesPrice'], + 'taxAmount'=>$cart_prices['taxAmount'], + 'salesPriceWithDiscount'=>$cart_prices['salesPriceWithDiscount'], + 'discountAmount'=>$cart_prices['discountAmount'], + 'priceWithoutTax'=>$cart_prices['priceWithoutTax'], + 'discountBeforeTaxBill'=>$cart_prices['discountBeforeTaxBill'], + + 'country'=>$address['virtuemart_country_id'], + ); + $cartvals = array_merge ($cartvals, $this->getOrderDimensions ($cart)); + // Let child classes update the $cartvals array, or add new variables + $this->addCustomCartValues($cart, $cart_prices, $cartvals); + return $cartvals; + } + + + /** + * @param \VirtueMartCart $cart + * @param int $method + * @param array $cart_prices + * @return bool + */ + protected function checkConditions ($cart, $method, $cart_prices) { + if (empty($method->rules)) $this->parseMethodRules($method); + + $cartvals = $this->getCartValues ($cart, $method, $cart_prices); + foreach ($method->rules as $r) { + if ($r->matches($cartvals)) { + $method->matched_rule = $r; + $method->rule_name = $r->name; + return TRUE; + } + } + vmdebug('checkConditions '.$method->name.' does not fit'); + return FALSE; + } + + /** + * Create the table for this plugin if it does not yet exist. + * This functions checks if the called plugin is active one. + * When yes it is calling the standard method to create the tables + * + * @author Valérie Isaksen + * + */ + function plgVmOnStoreInstallShipmentPluginTable ($jplugin_id) { + return $this->onStoreInstallPluginTable ($jplugin_id); + } + + /** + * @param VirtueMartCart $cart + * @return null + */ + public function plgVmOnSelectCheckShipment (VirtueMartCart &$cart) { + return $this->OnSelectCheck ($cart); + } + + /** + * plgVmDisplayListFE + * This event is fired to display the pluginmethods in the cart (edit shipment/payment) for example + * + * @param object $cart Cart object + * @param integer $selected ID of the method selected + * @return boolean True on success, false on failures, null when this plugin was not selected. + * On errors, JError::raiseWarning (or JError::raiseError) must be used to set a message. + * + * @author Valerie Isaksen + * @author Max Milbers + */ + public function plgVmDisplayListFEShipment (VirtueMartCart $cart, $selected = 0, &$htmlIn) { + return $this->displayListFE ($cart, $selected, $htmlIn); + } + + /** + * @param VirtueMartCart $cart + * @param array $cart_prices + * @param $cart_prices_name + * @return bool|null + */ + public function plgVmOnSelectedCalculatePriceShipment (VirtueMartCart $cart, array &$cart_prices, &$cart_prices_name) { + return $this->onSelectedCalculatePrice ($cart, $cart_prices, $cart_prices_name); + } + + /** + * plgVmOnCheckAutomaticSelected + * Checks how many plugins are available. If only one, the user will not have the choice. Enter edit_xxx page + * The plugin must check first if it is the correct type + * + * @author Valerie Isaksen + * @param VirtueMartCart cart: the cart object + * @return null if no plugin was found, 0 if more then one plugin was found, virtuemart_xxx_id if only one plugin is found + * + */ + function plgVmOnCheckAutomaticSelectedShipment (VirtueMartCart $cart, array $cart_prices = array(), &$shipCounter) { + if ($shipCounter > 1) { + return 0; + } + return $this->onCheckAutomaticSelected ($cart, $cart_prices, $shipCounter); + } + + /** + * This method is fired when showing when priting an Order + * It displays the the payment method-specific data. + * + * @param integer $_virtuemart_order_id The order ID + * @param integer $method_id method used for this order + * @return mixed Null when for payment methods that were not selected, text (HTML) otherwise + * @author Valerie Isaksen + */ + function plgVmonShowOrderPrint ($order_number, $method_id) { + return $this->onShowOrderPrint ($order_number, $method_id); + } + + function plgVmDeclarePluginParamsShipment ($name, $id, &$data) { + return $this->declarePluginParams ('shipment', $name, $id, $data); + } + + + /** + * @author Max Milbers + * @param $data + * @param $table + * @return bool + */ + function plgVmSetOnTablePluginShipment(&$data,&$table){ + + $name = $data['shipment_element']; + $id = $data['shipment_jplugin_id']; + + if (!empty($this->_psType) and !$this->selectedThis ($this->_psType, $name, $id)) { + return FALSE; + } else { + // Try to parse all rules (and spit out error) to inform the user: + $method = new StdClass (); + $this->parseMethodRule ($data['rules1'], $data['countries1'], $data['tax_id1'], $method); + $this->parseMethodRule ($data['rules2'], $data['countries2'], $data['tax_id2'], $method); + $this->parseMethodRule ($data['rules3'], $data['countries3'], $data['tax_id3'], $method); + $this->parseMethodRule ($data['rules4'], $data['countries4'], $data['tax_id4'], $method); + $this->parseMethodRule ($data['rules5'], $data['countries5'], $data['tax_id5'], $method); + $this->parseMethodRule ($data['rules6'], $data['countries6'], $data['tax_id6'], $method); + $this->parseMethodRule ($data['rules7'], $data['countries7'], $data['tax_id7'], $method); + $this->parseMethodRule ($data['rules8'], $data['countries8'], $data['tax_id8'], $method); + $ret=$this->setOnTablePluginParams ($name, $id, $table); + return $ret; + } + } + + +} +} + +if (!class_exists ('ShippingRule')) { + +class ShippingRule { + var $rulestring = ''; + var $countries = array(); + var $tax_id = 0; + var $conditions = array(); + var $shipping = 0; + var $includes_tax = 0; + var $name = ''; + + function __construct ($rule, $countries, $tax_id) { + if (is_array($countries)) { + $this->countries = $countries; + } elseif (!empty($countries)) { + $this->countries[0] = $countries; + } + $this->tax_id = $tax_id; + $this->rulestring = $rule; + $this->parseRule($rule); + } + + function parseRule($rule) { + $ruleparts=explode(';', $rule); + foreach ($ruleparts as $p) { + $this->parseRulePart($p); + } + } + + function handleAssignment ($var, $value, $rulepart) { + switch (strtolower($var)) { + case 'shipping': $this->shipping = $value; $this->includes_tax = False; break; + case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; break; + case 'name': $this->name = $value; break; + default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error'); + } + } + + + function parseRulePart($rulepart) { + /* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */ + /* In the advanced version, all conditions and costs can be given as a full mathematical expression */ + /* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */ + $operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '=='); + $op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/'; + $rulepart = trim($rulepart); + if (empty($rulepart)) return; + $atoms = preg_split ($op_re, $rulepart, -1, PREG_SPLIT_DELIM_CAPTURE); + if (count($atoms)==1) { + $this->shipping = $this->parseShippingTerm($atoms[0]); + } elseif ($atoms[1]=='=') { + $this->handleAssignment ($atoms[0], $atoms[2], $rulepart); + } else { + // Conditions, need at least three atoms! + while (count($atoms)>1) { + if (in_array ($atoms[1], $operators)) { + $this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2])); + array_shift($atoms); + array_shift($atoms); + } else { + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $rulepart), 'error'); + $atoms = array(); + } + } + } + } + + function parseShippingTerm($expr) { + /* In the advanced version, shipping cost can be given as a full mathematical expression */ + return strtolower($expr); + } + + function evaluateTerm ($expr, $vals) { + if (is_null($expr)) { + return $expr; + } elseif (is_numeric ($expr)) { + return $expr; + } elseif (is_string ($expr)) { + // Explicit strings are delimited by '...' or "..." + if (($expr[0]=='\'' || $expr[0]=='"') && ($expr[0]==substr($expr,-1)) ) { + return substr($expr,1,-1); + } elseif (array_key_exists(strtolower($expr), $vals)) { + return $vals[strtolower($expr)]; + } else { + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error'); + return null; + } + } elseif (is_array($expr)) { + // Operator + $op = array_shift($expr); + $args = array(); + foreach ($expr as $e) { + $term = $this->evaluateTerm($e, $vals); + if (is_null($term)) return null; + $args[] = $term; + } + $res = false; + switch ($op) { + case 'OR': + case ' OR ': foreach ($args as $a) { $res = ($res || $a); }; break; + case 'AND': + case ' AND ': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break; + case 'in': $needle = array_shift($args); $res = in_array($needle, $args); break; + + case '<': $res = ($args[0] < $args[1]); break; + case '<=': + case '=<': $res = ($args[0] <= $args[1]); break; + case '==': $res = ($args[0] == $args[1]); break; + case '!=': + case '<>': $res = ($args[0] != $args[1]); break; + case '>=': + case '=>': $res = ($args[0] >= $args[1]); break; + case '>': $res = ($args[0] > $args[1]); break; + + case "+": $res = ($args[0] + $args[1]); break; + case "-": $res = ($args[0] - $args[1]); break; + case "*": $res = ($args[0] * $args[1]); break; + case "/": $res = ($args[0] / $args[1]); break; + case "%": $res = ($args[0] % $args[1]); break; + case "^": $res = ($args[0] ^ $args[1]); break; + + default: $res = false; + } + return $res; + } else { + // Neither string nor numeric, nor operator... + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error'); + return null; + } + } + + function calculateShipping ($vals) { + return $this->evaluateTerm($this->shipping, $vals); + } + + function matches($vals) { + // First, check the country, if any conditions are given: + if (count ($this->countries) > 0 && !in_array ($vals['country'], $this->countries)) + return False; + + foreach ($this->conditions as $c) { + // All conditions have to match! + $ret = $this->evaluateTerm($c, $vals); + if (is_null($ret) || (!$ret)) { + return false; + } + } + // All conditions match, so return true + return true; + } + + function getShippingCosts($vals) { + return $this->calculateShipping($vals); + } + +} + +} +// No closing tag -- GitLab