From 8edf8b9efaa3ff8ee7170b5164d5393d0af4730a Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Wed, 13 Jan 2016 18:43:41 +0300 Subject: [PATCH] v2.0 --- README.md | 58 +- composer.json | 35 - composer.lock | 67 -- docs/images/add.png | Bin 0 -> 11684 bytes docs/images/ref.png | Bin 0 -> 39918 bytes docs/images/setup.png | Bin 0 -> 11224 bytes intarocrm/config.xml | 13 - intarocrm/export.tpl | 39 - intarocrm/index.php | 9 - intarocrm/intarocrm.php | 1267 ------------------------ intarocrm/logo.gif | Bin 1124 -> 0 bytes intarocrm/logo.png | Bin 1963 -> 0 bytes intarocrm/sync.php | 9 - intarocrm/translations/ru.php | 36 - retailcrm/bootstrap.php | 99 ++ retailcrm/config.xml | 13 + retailcrm/config_ru.xml | 13 + retailcrm/job/export.php | 147 +++ retailcrm/job/icml.php | 11 + retailcrm/job/sync.php | 398 ++++++++ retailcrm/lib/CurlException.php | 5 + retailcrm/lib/InvalidJsonException.php | 5 + retailcrm/lib/RetailcrmApiClient.php | 811 +++++++++++++++ retailcrm/lib/RetailcrmApiResponse.php | 122 +++ retailcrm/lib/RetailcrmCatalog.php | 133 +++ retailcrm/lib/RetailcrmHttpClient.php | 113 +++ retailcrm/lib/RetailcrmIcml.php | 137 +++ retailcrm/lib/RetailcrmProxy.php | 43 + retailcrm/lib/RetailcrmReferences.php | 215 ++++ retailcrm/lib/RetailcrmService.php | 42 + retailcrm/logo.gif | Bin 0 -> 306 bytes retailcrm/logo.png | Bin 0 -> 5071 bytes retailcrm/retailcrm.php | 367 +++++++ retailcrm/translations/ru.php | 49 + 34 files changed, 2759 insertions(+), 1497 deletions(-) delete mode 100644 composer.json delete mode 100644 composer.lock create mode 100644 docs/images/add.png create mode 100644 docs/images/ref.png create mode 100644 docs/images/setup.png delete mode 100644 intarocrm/config.xml delete mode 100644 intarocrm/export.tpl delete mode 100644 intarocrm/index.php delete mode 100644 intarocrm/intarocrm.php delete mode 100644 intarocrm/logo.gif delete mode 100644 intarocrm/logo.png delete mode 100644 intarocrm/sync.php delete mode 100644 intarocrm/translations/ru.php create mode 100644 retailcrm/bootstrap.php create mode 100644 retailcrm/config.xml create mode 100644 retailcrm/config_ru.xml create mode 100644 retailcrm/job/export.php create mode 100644 retailcrm/job/icml.php create mode 100644 retailcrm/job/sync.php create mode 100644 retailcrm/lib/CurlException.php create mode 100644 retailcrm/lib/InvalidJsonException.php create mode 100644 retailcrm/lib/RetailcrmApiClient.php create mode 100644 retailcrm/lib/RetailcrmApiResponse.php create mode 100644 retailcrm/lib/RetailcrmCatalog.php create mode 100644 retailcrm/lib/RetailcrmHttpClient.php create mode 100644 retailcrm/lib/RetailcrmIcml.php create mode 100644 retailcrm/lib/RetailcrmProxy.php create mode 100644 retailcrm/lib/RetailcrmReferences.php create mode 100644 retailcrm/lib/RetailcrmService.php create mode 100644 retailcrm/logo.gif create mode 100644 retailcrm/logo.png create mode 100644 retailcrm/retailcrm.php create mode 100644 retailcrm/translations/ru.php diff --git a/README.md b/README.md index f1ae684d..5ba61cbf 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,54 @@ Prestashop module -============= +================= -Prestashop module for interaction with [IntaroCRM](http://www.intarocrm.com) through [REST API](http://docs.intarocrm.ru/rest-api/). +Модуль интеграции CMS Prestashop c [RetailCRM](http://www.retailcrm.com) -Module allows: +Модуль позволяет: -* Send to IntaroCRM new orders -* Configure relations between dictionaries of IntaroCRM and Prestashop (statuses, payments, delivery types and etc) -* Generate [ICML](http://docs.intarocrm.ru/index.php?n=Пользователи.ФорматICML) (IntaroCRM Markup Language) for catalog loading by IntaroCRM +* Экспортировать в CRM данные о заказах и клиентах и получать обратно изменения по этим данным +* Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) +* Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) -Installation -------------- +###Установка -### 1. Manual installation +#####Скачайте модуль +[Cкачать](http://download.retailcrm.pro/modules/prestashop/retailcrm-2.0.zip) -#### Clone module. -``` -git clone git@github.com:/intarocrm/prestashop-module.git -``` +#####Установите через административный интерфейс управления модулями. + +![Установка модуля](/docs/images/add.png) + + +###Настройка + +#####Перейдите к настройкам + +![Настройка модуля](/docs/images/setup.png) + +#####Введите адрес и API ключ вашей CRM и задайте соответствие справочников -#### Install Rest API Client. +![Справочники](/docs/images/ref.png) -Install api-client-php via [composer](http://getcomposer.org) + +#####Регулярная генерация выгрузки каталога + +Добавьте в крон запись вида ``` -cd prestashop-module -/path/to/composer.phar install +* */4 * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/icml.php ``` -#### Create .zip file. +#####Регулярное получение изменение из RetailCRM + +Добавьте в крон запись вида + ``` -zip -r intarocrm.zip intarocrm +*/7 * * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/sync.php ``` -#### Install via Admin interface. +#####Единоразовая выгрузка архива клиентов и заказов в RetailCRM - -Go to Modules -> Add module. After that upload your zipped module and activate it. +``` +/usr/bin/php /path/to/your/site/modules/retailcrm/job/export.php +``` diff --git a/composer.json b/composer.json deleted file mode 100644 index 5b855e4b..00000000 --- a/composer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "intarocrm/prestashop-module", - "description": "Prestashop integration for IntaroCRM", - "type": "library", - "keywords": ["api", "Intaro CRM", "rest"], - "homepage": "http://www.intarocrm.ru/", - "config": { - "vendor-dir": "intarocrm/classes" - }, - "authors": [ - { - "name": "Alex Lushpai", - "email": "lushpai@intaro.ru", - "role": "Developer" - } - ], - "support": { - "email": "support@intarocrm.ru" - }, - "require": { - "php": ">=5.3", - "intarocrm/rest-api-client": "1.2.*" - }, - "autoload": { - "psr-0": { - "": "src/" - } - }, - "repositories": [ - { - "type": "git", - "url": "https://github.com/intarocrm/rest-api-client" - } - ] -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 239a7397..00000000 --- a/composer.lock +++ /dev/null @@ -1,67 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" - ], - "hash": "e4583dae732a6e5ebafff8cca46b1984", - "packages": [ - { - "name": "intarocrm/rest-api-client", - "version": "v1.2.5", - "source": { - "type": "git", - "url": "https://github.com/intarocrm/rest-api-client", - "reference": "b54350ff2f09d8202cf2931895bba8dced4dcf21" - }, - "require": { - "ext-curl": "*", - "php": ">=5.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "IntaroCrm\\": "lib/" - } - }, - "authors": [ - { - "name": "Kruglov Kirill", - "email": "kruglov@intaro.ru", - "role": "Developer" - } - ], - "description": "PHP Client for IntaroCRM REST API", - "homepage": "http://www.intarocrm.ru/", - "keywords": [ - "Intaro CRM", - "api", - "rest" - ], - "support": { - "email": "support@intarocrm.ru" - }, - "time": "2014-04-13 09:58:37" - } - ], - "packages-dev": [ - - ], - "aliases": [ - - ], - "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": { - "php": ">=5.3" - }, - "platform-dev": [ - - ] -} diff --git a/docs/images/add.png b/docs/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..c518ef58b8dd97c0d5d2f3fd7ab7627e793058fe GIT binary patch literal 11684 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 ys;sZ?ot&)J*cJR3pA(EnUB4tAQf&T-lttM3f literal 0 HcmV?d00001 diff --git a/docs/images/ref.png b/docs/images/ref.png new file mode 100644 index 0000000000000000000000000000000000000000..ac8b229460faf45eb8dc9f5afce1dd26b1fc8685 GIT binary patch literal 39918 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 UzHU3a3IV_S8V2g6s!u}y4{@in@Bjb+ literal 0 HcmV?d00001 diff --git a/docs/images/setup.png b/docs/images/setup.png new file mode 100644 index 0000000000000000000000000000000000000000..99c7fc91102fa8142e041fb3e3e5e8abcda7e5ca GIT binary patch literal 11224 zcmZXabyQT}*TyjbX{13yq>)Z(0qO4UlmUkB5Ky|iySql30V(MQX^ zHJ-tH+g012)C-3R<_V+6w4i*+y1;KML}-lR#;huZItZeo{G1~rzRyIv96jg?5-Q`@ z7cDty-X|?3tIp#mnHK`kf>>&qJNO^0y0acpqtoVGTi7ewgfO$OWu&sJtp zAW)}1_7B`A9U=8bA|(jS1Zd8?tm|tz{zzRzHBhlKb&e0_)V}wZ$hqPJ(e>Uzd6G{? zUIzqWjtAS$5Z$9Ke+>&njU;?#mOhZ0`UCfc9BO`hoT<;=ZrPSsViV-OnUA@9US8g) z_Yuhb@G~y)f~Ar7xLwlu^Rgqv<6z}<#m;`B!wMhV)Qk&a1UT;)#^Bzfi|hZyX;}wiyuKV~B@D z<*t1TU{>^zNDPSpVYJm4g`!T*2o=*mWk08lb@UteFPZcF>%Hy*lPzRZC&g82P}#;;T| z89YC=YtL4FDkK74f-xWijTDQ&9Q$kjVQGW={q|H=X>3;s3v^K>m6r`yY5FO=%CvYo9u z7ZwZsE!eJqFAz)SCWu|OJO!#{TUfbDZEo_Jr2V%ICB&S5A2N6<&F)5Yn%lf%lhV2e z%?3h4(|?qd?2^$e8>W_VrS6dvNlO@Cd?%jF6mokh?>h+*cE3ZaG(5xa^z-{NnjN&x z?zxoXbsPd5Q^jZxO|g-bs~1?dkmG`Vv>)1R;6W>pDm8?9xgjX2JMH2he?-T9WK_9eN%9JXTET)+Anc+vT($a zDL#15=fk!jbGlZ?9oBn;q0K}vVmaVg{*E_%i<0u5?fBy*sy+*~%wHG=>o@Zj5$?wn zs?L||Hn*NU7VT@s3alq<6gHrY{4|UrJp8s z+V7v{glb?_SDxdeV}HFwg?q%rWRw&v4!?(C`esIf%*x8j-1aX1{AuP%zKuv3f|fBL zw`67v2hu3j(XeOh(a<0$j*|<;i#eQh-ax?PkP5BS)6>G{8>BR1Vr?eN4_1<|u-nc- z$-?)q{?6A(l&Q11>QWz*le3h()QzYulqlqcJ&};q|r@*P%xj{SI$(hLd%E7sVQoTF@Cs`Zug}o_lF3HUp>=7ls%FN znzK|8-O;FgvFJk5!pg?PuW>3aa4d&Yv)OYCCcisAL;hak?@(*Pcxlvk< zwWm4rX33zu$$NpHb{iccj{zCH;CjbL-D>^LJ$cnP%?elB>EWTFE%EwhOmz;DvNtHA*XP3uNT+unHF?1uPmFwg3ka2l zErHk14=liMyjX9ccu2iET6%Rl88Tdz}{%Pj%?Us#=4Lvh+QDbAH zs0G;%%?Qi6N=wg&CMDs`t|wn>h^`lWZ}7zM5reQ(clfHqkbt>FM-PRrAv?|uAc#AhPWd#-qCNY+oow!tPUFHL}uiJ);@ID&y)g~g@Yz$M2P z5i@r(E;-pUNpxuF?0$aId-(^?S{}SwJ>UDF%976j;zR7!A#g#!Y_hqi&83Th6!fB? zd!0?(^1yv*ybFWutyoI6!9sj(SbDn6&WKU(lh7ImC%>d03-cu#;aE;W&PuH%H(Iiw zgoXwUf7UyJCOg_@ccZ&2imv2dkD=iq?emd~^9zCq#3I$OvY*N4=T1J4564DdtqR-x zP=uQ&nA{cOvX37!{XZl4kqL0uOq^_8*d^reju^e^dgzne_P)RNo8N1=AY`SnUm-z7 zLrsNxFjOS7QVCtM;nFH3p`pHanIUSKcwuTEVu8DAd*|7*paVxe?it*ii~QZ@hVTE3 zFhf#ddfL)pwKvx>;9IqU&#Gv%6vTNQULd7P#OGxjkuy9xmhU}on9tliKXw1Ou82yC zjrhby*!mf-bzKw!Qz(<;{-Iot{ib)`;5~L|xS#u<$b~<&vcto}+bAkaL^(%>3JMUP zm7ks4T~8hQgQIaPv?oiO=Db7M=k**5SXxhWR?n*^rR{ctC8aV~-ehn{jN7!=cf7D2 zH2(%2)pXz|^B2ic9Pp!=bZapu9QNQKDNL0W8yhQNBbar!N6yy+XeZ5>MlvAlyS7*a zq`I9T6~!;ILJOVlTx<+|lZJjvqf_{K419&?Y(GBzT6EvcJ5*38!GgPLl209_C|t`~wBY3?H<#@IBl?{Xga? z<-F}XgL7Gm5N{?T1D74^I9Wcy;{?|(!AGsGUQqK_uAz)BD*8L$*&%n3hIs3lDo?Ak z={U-Zp?And5(WIkf^<=Z%M4aQT})Vc_5xun+78X`sI)oRoiugHqJ`rE+fvK( zUAaGP?ltYFQf2D8+$9RMZpX`m5WfV^4ReeW%$c)u7)i(_nm^HcuqH}PR@KH@5B%ZI z1>>2dc((=4I2P>9ZtdQp_E1Pfk(eRdLcSgK{F8%ja%Gl3cw$0X$D%Wj-&IL!#Q>w9 zSgZ0gr@t_241WPc)sQ^A>$A!vB364o?@*PPi-`YA@mC-Rq1CXG^RTjONX+U)U=hLf z0ab}Lh{ ziqcWGtNqb^e511&XH8h&Ra~kd zcP7h9>+n23GE!WQy8_P&*4Pfc-MlM5F9DP1(8k^L_;ctbNVCpia2ju|eX#FjoZZqV zD@@*7Ep5Kqkg`-xtHdF1tUkY6IY@DrHl)sZi2(RuI3Q$(IH^<`c7KD~-AzWMoD7(A47RZVqk|sN_qTES z9PnIhY4VtDybF!L#&U$HYToFzc@-VTU@Se~K{0MY(2#o|XHd(0aNC||d4zn;pLM4L zc3XR?RY*nYtW9uqsUClYIwJ%9Z+8b?CTcuDNdcp<1^58&1qq2Cd&@2eOn&=1@$a-p zEqg{4)=HsQH;otD``h}S+bJO-o8hGaLD>#4#rGbE_j^}g#mdXjHtgt_!+ztn{Thtv$ zX(0M3lg~GPEI^cnCQvaTK!2!tZ(Z@dH8Xh{4Dz$mPghfy5ljktHhS+tuLno|%2~~P zkh-L0t1^fv=0f;JH!fT^ESDA2jAvFj@eCYhA{`|uUeo!?Jcu|yAC3IoOX#P_qTSdK zo+nC5<^&|AEPe?GXh8Kxub4p3k!<24_{2_?%$7?y$Z>0gPRaD?$-6Ov@Rf3#CujOX z!-+Soh2hZqW_JqBD*dtPabecgy8-NB@x}Nx1_;Cz=9+GpK|mm?pb$0vR@hZH;#Ill zg@U?xNWMWk(I)Sj^8I#-yT%}IwNXT$>Bi9ZaHTHH7C|MR`s&10NP=LwEyirQyyEsS zA*QsveCziH-hzKYch>-gA3|3!q|WE=?0IP1=W0;Owv@Nke!MCk2lf0Sy?qV`5ZV^g z!LIuD<~&{aS4bb+ZDl^N+Y15O!O=-eRe$+|KknZ-fy|SYj)-PgP_q-?{Pu0ZH${#h zxPy{EzlKXt_xgEhS=raPf#i=`4)>cB>_;ndeYv8kZb!covlTs`pQN|9-N{Xg;;UC$ z9f1Uk@N{wFZ_1H&bN}o|e=UU|D+e~=NYn0^vN>ve3fWk3_ZvrJ?fl->hFmZ(b97WO z3k%KEvAS3pujzPz>Rlg4^^cnKa3AQ(h3V9WCeTIm89dw1M_b;XZMFZ9k4dM=_*?JD zyYvgJ7-}Z#@fr|Qd7!3a#a*m-fN~=&@#HhCWV9MfH;NUJK-pQ8g68v@zpoLEd;F$h zR&}>4GT?jfww%H$^?bYQ;rlFv%3323eLBqA*#-7{lv{ngxLM$?g!Q;c_eYcZn~iJQ-!><-O({Du7*A$>Sd7Nl>}`Sog7! zpZQy*O7l*}HYTi;qQCVIm_&VteKws$$e##yZ^vi$uz%QUl#yRpS&9DRHBq^Qg!>); zA~iw-vBi9~G$JAq^bU3U=}{f}a84$51J3d3k6?XH&i^Im zl|jco@(m0a0=0{S0{&O%G&FsH+0NChAS;Gl`LtcQEpRitE--FNxB1j5NjN2| z_tmOqyI8mxl5N9PcMcRuJslslsvWkvgrA(alk25gbL;5^fhPz0i zOISb;cTXujUhzI1dv5Q0ZogpBV%a^v^zVG{vPDlIa*1A|NLm7HG$;wQ7yTgU_JGJE zYl@%X{R5UxgAL7BDK*$!l>lp}ev?VhlK%}Z^3_Sgxc^lI;py2|V0bbsopb*Hj)Z>B?76bKG+~=;VZ{vp14%!5+o=*+{sdM;IwyQgH$L!u%v`=iHC0ec(RtV z=X=z_+4&W%Wp_U9ve&2sUg;JxtrtgU zNBH*#UDN0p;u-WaE?goaX-!Q6aK|M<3X8$t=Z;67AY`nauI?aXj`L#)M5v&K0&-QL z;^krOJ^171jy#w;w`Y#yrHiY@#*)>o_=qCkPr z-=yVuTD-7LBqpA;qCumiq*MYhfoeUM%UD~<>+FcY)|!~_U&Q*2@(|6e%O{jaV6bNJ z;+IP_R^_Mfu5Uvase2a}E7bpv;tDzdS0@d%a4X+J=m}gDKz})&&W%5mTgrU@ z@e;7^R-mb3DGscp{bUUKlgVqzGX<7#e4X=+6o*&P@9#(taZ&gmeWU#4n#}4cKKt*v ztf;ABvb+8IBKbY~b!D<8kULVzI0>sX2LSHo;5^Ogf2PXF$mrng++)j=H{DVP;16!@ z*{Et4IXNNuy;62eJ^D-3V{uJOq>jLQHsFhJHs>~{$5IAOrb&MZxbJXj)#zJ#%`jCP zw#W~4n`aYoL_WPj=JT!H2alVh5q7ET$xRi1YWMYdlOxNE4T>SDdk-G0ED3!B>; zv+g6b__<5;<`%O1(DKhegpzjzS|9>cRd$C(KT<5OYr0_!LDJKcm^?ecNq7tpkK)Kz zO+LZfl$0kWX5{-4%$^2%ou%7(ReO>J0r|fU;CuM!U7>MQj?U8!z`X#D7obrbz+HBg zla(w`80YSV~IjcGiqaw!QS&_>bfb ze3a1(&O2eM1cFzBQy2RA@o2AOL?sB{yap86e~*`dC;6W+&SlQ~U%4S@A*-=UX&mMC z%Ye`CsojAmY~cN({)rA2T{{1c1HHrkn4G;Z<|2_6aQeano8n^&ja4a_uyL(NU=uW_ z-W|*AUm6H;#HAaa`(Ufdg3#)A&|e!nLHp?BJWVBo<9m-~{JVAoLBGQZfW6TwIyzx( zZ35jL_d^joN;!hvx6W3|jEVGk;YiG`e+Cd8oUHuU+-mr~HW~S8uklN%IfCi+y`LF} zhGkZ-$qEJ(yJ~#zX&-UlynZ>Ei>4n>)D*YC?o39KRe1AhK^8-sc@~}Y;k89kk?m4u z*Do5S;p@W{JMD&68K{j-C2ep=N5?iu)S&h6@o{&RULPI-UaTKHbU75}!tbI>hL5kX zboCQ!z>oSDK*yEox9)#>kIHzfaS^zy*;NJo$h#|`(!<$I`@|Os%w76+4lr7ROrez5 z$e)ds2^GoZ25foEIMP0OGOcv$lWH@o>M#*-up0EPiL6&Ycmcr!%v#PS%$4V9zDDPV zZdI|C0|me9&&A-zw33RdDv6r;h8x)K@gHnOH%(dtecR&2%o`pFamN(ue{~?tKGtR$ItZy!}e&>m4djWE7w!#4v@7BOCnIH@x&RY>j_jHJt?ZNJ>r=qr$X zXnVjdBeqb4wtwo7DC8ZUA`c7`4?v_vbFkyy#t@N21+R8^CuC=@o+GV5jX6vTEH@7h z4jPebh9wR1atjN;uat%1xyssK_7Xoog4KJ1^GJkAi=ZNaTLy6wLjksnNvp?8q*XC{ z(+C>C6%WSYGo$6_ZL++L^W#by8j_`*U7BxU=c&{azrP|&B@xE>rtvXlI!_$=LxJV& zc1CyCG1%Sdyw_1SJYv!s^7;lDn_D1#)%T9)ey6?l7ukMSo??>GPT>OpMA2k< z4tLz7tIb6mp8B^?CY;i3CqVY}j?T^nt*xyRIN!^@AZO{;Q9kdyo7o&Fcw87qV9ODX z+6cm8D=GY@ih|l_7b2;im$$d-@oF}fa?blY?hKcX-G9UM_Tlf36VKhF=V6NH^X{Vn zL^Qp4oKId`ZwMlOtPA_5HW^b23m<7iEx7@01(^23wYHFM@nCFD@C(oR>N-A0T)=Jj z*|JYp>A3^)j0<=(0vi!JQB*L}CSH>X88IR2>C*h_dmBwS*#MdGB3yc|CaMUK`8UWj z%rzg|{jzSN-lgIYL{A!9J9gsZ<2yO583H_&l1gfLc<^|}0E7eeV~%UE+&n%O)%nV> z)I`f}wffS9zsdVAlv;+m-|oHFu@A8Qf;x4^;;pjXotn(#a3~7~CIB@Dlu>}vhZH86 z@!l!67aY|-d96xRY}xAwDfaqC7?vey0H=``aXePh$X$#Nt^Wwht%o?iz+K5R4THy1F|RcQW8NcFJ*ll41{)~Wz& zl&X&4d6yfueU$>`D}WIxf4&^|kKR7Fzouv3P3N_D&LN*N zpQ`G=5u87pIp z-1niK-!5>L=v?(dZ%)=a=gbAm%NhA<{VD#r0d>Y%vVjgokLmLZLccyb3g6!+YqwND z;4d!f3g7OD8+zU1V^W`hCf4jSaNm$|2S+!}FAy3wk+_DX3x!0JJ^piSkKS&)BuzW5 zAZ508yA~Falf|R!qvuo%IWQys{4R&TPh;b@EUnHSZyTiJzc1KYPOMV`NgT-ixvj0` z5*XFMf!glpf7BfCPN$ck7@$N*-CJe#=M-C!RB2#-K1N2Z#{&B9V@&Xr38?nqej)SP zVZ2E|2Vmy(hr)L}5upkzgpDjEzXUyhE#3K^$}<*BF>zDdsj`hpfBuZ1qM~whcXxg! z@+1@KlI)n`9XO}JwSOIF=}U=@Bpwl?*<;%6w1Ym>skuqXP2uzH_Q8{Da?-vZ;*ITXIJz+wX%I?gAK-1SpWMktnKcA*FyYKRF zlrV0+24GHqM#dq}gG((wm-oaC3$|ogY-y!cRd$+!01~ry{|IFuU|k#KWt?HeF8H7dI9`L0)IrnChE)zo+L+7CC*ocW=Nudf6_lqDWSv-$DXT3 zpZue5Eq1*)T%ym^+*nSiVfq53x zL|<$eVk9QrUNw~krl*rEdV6zt!L2!ZRy@TtUO%YT7ha#bzIFQ-9ia`#cHmIPcc#H-GzjplP$}x!bLdofNKR=%1k1 zBzptje3po(UUc8gW3>tJL;AUdg^^tSq}brlnK5P_ z)7l)^ACb^qnhA+!e&E8s0{mN^7ME^nTGzH^%#bL9n=ZIe!BJgHYjSL!FiTSk`r_5U zwFxBtR8k&{=WmKl(=~CU=R1~GaT!?>H*jKkl$b7&+-ZYZ%80&SNz0^v=c-H|zRP$l zq&-3wM#1);TuIUWz@)IUvPwpgNj=;_whY>It%@eBd)4)pwtTxg2GpvI$6sRsv^kO> z?0JiXfk*bsO$ECgn{gAG+C;pB{X|23{qNDyZ9?b`qCuOR1+4PTUu5FFXOV=3P51D4 z2mBuT+pY9~?kZpB+KIw+DC10U`&Y|CBT1VR9GYebQ|&p6r+)sA%O zjsM-=me?K3nmo9Sk{ykwRxvP85EXr~jlZ3FX*g-U?ryH#Z<^=UgETWaNe}mumyuD2 zo};2-VJ-98)nhiTy&^9(AqKQ&^8wOb!sFel(NdU2usG%O8?Df6& zL3mjk`^z(UxW%L~xIpcfF&H1#G~yIg{zrX(<~Py_BVEOmSZzo=oFTh$Nwq&JZ592G zfnr?uehO5vyTyT(-XXEKAIyd47) za>-S*;P&aMsq{;IFE9Q_WGq*_p$xCm{3XY`J;L+H+mk^6!#B?7D$~C~M=xCa)~R2y z{A#=EG?_7}lvc5~9Za!L%&9CTRd9X&>vShWeKhrjEX~l*f z+sm?a+ReuXa$UbEgW(^jXg$~J+ERZznRg55;6NnCQUB3VmsmFEGBr(Armik+TChkWHT&p?VntHO@(Z|^(A&3f50_h} z`G4RN^aE9QVlHcJKpZ8|DXD6z$V^DJ(VwuB3hD|M9+lIr>{v_maJbKkgNOGFy)md! zE`FeCD`tr?eiX+i3AZ`a#^WbTmDlFrEc3gy!1MEK?zpekSOSWpZ(eWBY7^ZV^pUwB z;%ELIqvGI*uhMH8N~x%*kdc>99LL4Q{W@Pw#>dBpAb7i#fVaw6ux#CVVQ8%f9-(-T z1%+w-c&P#P!|Z^!&XlQK9faJMYe;i*(=Q*NEY1RcZL&a%ZIh0l<&r{X17aJQG?1MuPc(aKqeFOmDi(l)C!rzc1@M?z1zjNQXncQJM3FX_%NF7!rE>gzd>#SLQ& zf}V1T1nggioxKBO&zm1&qBcX6mO{im+oTm8w}3!YDNFnwS+5xmpXt$N!U2$h%N}gW z>*DA0z?AA4&=v!9cj=D}(q5<{sg)>z;c0iz86*ZCfZtL9n$iaEI}6}*RY;H4`wx;T zlnlrTC=sDwYTW7Q1QIeUnSOov@Gf#1U^5=3du&Kp#QlKPQcp}724yx;_fLOp#(G4? z%r9(CCDTjLzB|N9kCbzs+S<>Kn&}B)%NPOLCkUv+a0jMayl(4<$&ae-^w={-ts6;z zuLZ8G*OuqvD7s2M6DJse^gugbl}rX8wgZ}Cuq1xWlRcDHSBq85=Bbvkkzm-a;*GaU z$mw4Vy$A=d_Ns$yCK&b292+J(tXRjg`MZlEBmN=e$`N!fU1oqOd)@iraslQhJcE?Uucly z)`Z;}XG~zL%QnzUgpins zXU0Xcy;`SRwKLd)cIuWluM(RxkPUVps*qJ@dswzW417lY{GvKfnSPIy zs5jwaXShT1KHKuqaQFA5*`G!evV`K`g1H&Hm{ccYk?9WwB$4Al;VqTfG7*)AN{Uxd zKN+Zt9!soY0F98l<%UY84@f{0&*?Rm15to==tv}*eyhh@Fc=)tvIGeFs$cRFdVK(2*v!?@xh4W84I{K&2*U-sw)g*yKH{9#oY4EwKC9SJbQR~I_~$fM - - intarocrm - - - - - - Вы уверены, что хотите удалить модуль? - 1 - 1 - - \ No newline at end of file diff --git a/intarocrm/export.tpl b/intarocrm/export.tpl deleted file mode 100644 index 1ee8a3f6..00000000 --- a/intarocrm/export.tpl +++ /dev/null @@ -1,39 +0,0 @@ - - - - {$shop_name} - {$company} - {$shop_url} - PrestaShop - - {foreach from=$currencies item=cur name=currencies} - {if $cur.iso_code != 'GBP'} - - {/if} - {/foreach} - - - {foreach from=$categories item=cat name=categories} - {if $cat.id_category!=2} - {$cat.name|escape} - {/if} - {/foreach} - - - {foreach from=$products item=offer name=products} - - {$offer.url} - {$offer.price} - {$offer.purchase_price} - {$currency} - {$offer.id_category_default} - {$offer.picture} - {$offer.name|escape} - {if $offer.article} - {$offer.article|escape} - {/if} - - {/foreach} - - - diff --git a/intarocrm/index.php b/intarocrm/index.php deleted file mode 100644 index 5f95ef61..00000000 --- a/intarocrm/index.php +++ /dev/null @@ -1,9 +0,0 @@ -exportCatalog(); \ No newline at end of file diff --git a/intarocrm/intarocrm.php b/intarocrm/intarocrm.php deleted file mode 100644 index eb5940e7..00000000 --- a/intarocrm/intarocrm.php +++ /dev/null @@ -1,1267 +0,0 @@ -name = 'intarocrm'; - $this->tab = 'market_place'; - $this->version = '0.1'; - $this->author = 'Intaro Ltd.'; - - $this->displayName = $this->l('IntaroCRM'); - $this->description = $this->l('Integration module for IntaroCRM'); - $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); - - $this->intaroCRM = new \IntaroCrm\RestApi( - Configuration::get('INTAROCRM_ADDRESS'), - Configuration::get('INTAROCRM_API_TOKEN') - ); - - $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); - - $this->response = array(); - $this->customerFix = array(); - $this->orderFix = array(); - - $this->address_id = null; - $this->customer_id = null; - - $this->customer = null; - - parent::__construct(); - } - - function install() - { - return ( - parent::install() && - $this->registerHook('newOrder') && - $this->registerHook('actionOrderStatusPostUpdate') && - $this->registerHook('actionPaymentConfirmation') - - ); - } - - function uninstall() - { - return parent::uninstall() && - Configuration::deleteByName('INTAROCRM_ADDRESS') && - Configuration::deleteByName('INTAROCRM_API_TOKEN') && - Configuration::deleteByName('INTAROCRM_API_STATUS') && - Configuration::deleteByName('INTAROCRM_API_DELIVERY') && - Configuration::deleteByName('INTAROCRM_LAST_SYNC') && - Configuration::deleteByName('INTAROCRM_API_ADDR') - ; - } - - public function getContent() - { - $output = null; - - $address = Configuration::get('INTAROCRM_ADDRESS'); - $token = Configuration::get('INTAROCRM_API_TOKEN'); - - if (!$address || $address == '') { - $output .= $this->displayError( $this->l('Invalid crm address') ); - } elseif (!$token || $token == '') { - $output .= $this->displayError( $this->l('Invalid crm api token') ); - } else { - $output .= $this->displayConfirmation( - $this->l('Timezone settings must be identical to both of your crm and shop') . - " $address/admin/settings#t-main" - ); - } - - if (Tools::isSubmit('submit'.$this->name)) - { - $address = strval(Tools::getValue('INTAROCRM_ADDRESS')); - $token = strval(Tools::getValue('INTAROCRM_API_TOKEN')); - $delivery = json_encode(Tools::getValue('INTAROCRM_API_DELIVERY')); - $status = json_encode(Tools::getValue('INTAROCRM_API_STATUS')); - $payment = json_encode(Tools::getValue('INTAROCRM_API_PAYMENT')); - $order_address = json_encode(Tools::getValue('INTAROCRM_API_ADDR')); - - if (!$address || empty($address) || !Validate::isGenericName($address)) { - $output .= $this->displayError( $this->l('Invalid crm address') ); - } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { - $output .= $this->displayError( $this->l('Invalid crm api token') ); - } else { - Configuration::updateValue('INTAROCRM_ADDRESS', $address); - Configuration::updateValue('INTAROCRM_API_TOKEN', $token); - Configuration::updateValue('INTAROCRM_API_DELIVERY', $delivery); - Configuration::updateValue('INTAROCRM_API_STATUS', $status); - Configuration::updateValue('INTAROCRM_API_PAYMENT', $payment); - Configuration::updateValue('INTAROCRM_API_ADDR', $order_address); - $output .= $this->displayConfirmation($this->l('Settings updated')); - } - } - $this->display(__FILE__, 'intarocrm.tpl'); - - return $output.$this->displayForm(); - } - - public function displayForm() - { - - $this->displayConfirmation($this->l('Settings updated')); - - $default_lang = $this->default_lang; - $intaroCrm = $this->intaroCRM; - - /* - * Network connection form - */ - $fields_form[0]['form'] = array( - 'legend' => array( - 'title' => $this->l('Network connection'), - ), - 'input' => array( - array( - 'type' => 'text', - 'label' => $this->l('CRM address'), - 'name' => 'INTAROCRM_ADDRESS', - 'size' => 20, - 'required' => true - ), - array( - 'type' => 'text', - 'label' => $this->l('CRM token'), - 'name' => 'INTAROCRM_API_TOKEN', - 'size' => 20, - 'required' => true - ) - ), - 'submit' => array( - 'title' => $this->l('Save'), - 'class' => 'button' - ) - ); - - /* - * Delivery - */ - $fields_form[1]['form'] = array( - 'legend' => array( - 'title' => $this->l('Delivery'), - ), - 'input' => $this->getDeliveryTypes($default_lang, $intaroCrm), - ); - - /* - * Order status - */ - $fields_form[2]['form'] = array( - 'legend' => array( - 'title' => $this->l('Order statuses'), - ), - 'input' => $this->getStatusTypes($default_lang, $intaroCrm), - ); - - /* - * Payment - */ - $fields_form[3]['form'] = array( - 'legend' => array( - 'title' => $this->l('Payment types'), - ), - 'input' => $this->getPaymentTypes($intaroCrm), - ); - - /* - * Address fields - */ - $fields_form[4]['form'] = array( - 'legend' => array( - 'title' => $this->l('Address'), - ), - 'input' => $this->getAddressFields() - ); - - - /* - * Diplay forms - */ - - $helper = new HelperForm(); - - $helper->module = $this; - $helper->name_controller = $this->name; - $helper->token = Tools::getAdminTokenLite('AdminModules'); - $helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name; - - $helper->default_form_language = $default_lang; - $helper->allow_employee_form_lang = $default_lang; - - $helper->title = $this->displayName; - $helper->show_toolbar = true; - $helper->toolbar_scroll = true; - $helper->submit_action = 'submit'.$this->name; - $helper->toolbar_btn = array( - 'save' => - array( - 'desc' => $this->l('Save'), - 'href' => AdminController::$currentIndex.'&configure='.$this->name.'&save'.$this->name. - '&token='.Tools::getAdminTokenLite('AdminModules'), - ), - 'back' => array( - 'href' => AdminController::$currentIndex.'&token='.Tools::getAdminTokenLite('AdminModules'), - 'desc' => $this->l('Back to list') - ) - ); - - $helper->fields_value['INTAROCRM_ADDRESS'] = Configuration::get('INTAROCRM_ADDRESS'); - $helper->fields_value['INTAROCRM_API_TOKEN'] = Configuration::get('INTAROCRM_API_TOKEN'); - - $deliverySettings = Configuration::get('INTAROCRM_API_DELIVERY'); - if (isset($deliverySettings) && $deliverySettings != '') - { - $deliveryTypes = json_decode($deliverySettings); - foreach ($deliveryTypes as $idx => $delivery) { - $name = 'INTAROCRM_API_DELIVERY[' . $idx . ']'; - $helper->fields_value[$name] = $delivery; - } - } - - $statusSettings = Configuration::get('INTAROCRM_API_STATUS'); - if (isset($statusSettings) && $statusSettings != '') - { - $statusTypes = json_decode($statusSettings); - foreach ($statusTypes as $idx => $status) { - $name = 'INTAROCRM_API_STATUS[' . $idx . ']'; - $helper->fields_value[$name] = $status; - } - } - - $paymentSettings = Configuration::get('INTAROCRM_API_PAYMENT'); - if (isset($paymentSettings) && $paymentSettings != '') - { - $paymentTypes = json_decode($paymentSettings); - foreach ($paymentTypes as $idx => $payment) { - $name = 'INTAROCRM_API_PAYMENT[' . $idx . ']'; - $helper->fields_value[$name] = $payment; - } - } - - $addressSettings = Configuration::get('INTAROCRM_API_ADDR'); - if (isset($addressSettings) && $addressSettings != '') - { - $addressTypes = json_decode($addressSettings); - foreach ($addressTypes as $idx => $address) { - $name = 'INTAROCRM_API_ADDR[' . $idx . ']'; - $helper->fields_value[$name] = $address; - } - } - - return $helper->generateForm($fields_form); - } - - public function hookNewOrder($params) - { - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionPaymentConfirmation($params) - { - $this->intaroCRM->orderEdit( - array( - 'externalId' => $params['id_order'], - 'paymentStatus' => 'paid', - 'createdAt' => $params['cart']->date_upd - ) - ); - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionOrderStatusPostUpdate($params) - { - $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); - $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int)$address_id; - $address = Db::getInstance()->ExecuteS($sql); - $address = $address[0]; - $delivery = json_decode(Configuration::get('INTAROCRM_API_DELIVERY')); - $payment = json_decode(Configuration::get('INTAROCRM_API_PAYMENT')); - $inCart = $params['cart']->getProducts(); - - if (isset($params['orderStatus'])) { - try { - $crmCustomerId = $this->intaroCRM->customerCreate( - array( - 'externalId' => $params['cart']->id_customer, - 'lastName' => $params['customer']->lastname, - 'firstName' => $params['customer']->firstname, - 'email' => $params['customer']->email, - 'phones' => array( - array( - 'number' => $address['phone'], - 'type' => 'mobile' - ), - array( - 'number' => $address['phone_mobile'], - 'type' => 'mobile' - ) - ), - 'createdAt' => $params['customer']->date_add - ) - ); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log("customerCreate: connection error", 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('customerCreate: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - try { - $items = array(); - foreach ($inCart as $item) { - $items[] = array( - 'initialPrice' => $item['price'], - 'quantity' => $item['quantity'], - 'productId' => $item['id_product'], - 'productName' => $item['name'], - 'createdAt' => $item['date_add'] - ); - } - - $dTypeKey = $params['cart']->id_carrier; - if (Module::getInstanceByName('advancedcheckout') === false) { - $pTypeKey = $params['order']->module; - } else { - $pTypeKey = $params['order']->payment; - } - $this->intaroCRM->orderCreate( - array( - 'externalId' => $params['order']->id, - 'orderType' => 'eshop-individual', - 'orderMethod' => 'shopping-cart', - 'customerId' => $params['cart']->id_customer, - 'firstName' => $params['customer']->firstname, - 'lastName' => $params['customer']->lastname, - 'phone' => $address['phone'], - 'email' => $params['customer']->email, - 'paymentStatus' => 'not-paid', - 'paymentType' => $payment->$pTypeKey, - 'deliveryType' => $delivery->$dTypeKey, - 'deliveryCost' => $params['order']->total_shipping, - 'status' => 'new', - 'deliveryAddress' => array( - 'city' => $address['city'], - 'index' => $address['postcode'], - 'text' => $address['address1'], - ), - 'discount' => $params['order']->total_discounts, - 'items' => $items, - 'createdAt' => $params['order']->date_add - ) - ); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderCreate: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderCreate: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - } - - if (isset($params['newOrderStatus']) && !empty($params['newOrderStatus'])) { - $statuses = OrderState::getOrderStates($this->default_lang); - $aStatuses = json_decode(Configuration::get('INTAROCRM_API_STATUS')); - foreach ($statuses as $status) { - if ($status['name'] == $params['newOrderStatus']->name) { - $currStatus = $status['id_order_state']; - try { - $this->intaroCRM->orderEdit( - array( - 'externalId' => $params['id_order'], - 'status' => $aStatuses->$currStatus, - 'createdAt' => $params['cart']->date_upd - ) - ); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderStatusUpdate: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderStatusUpdate: ' . $e->getMessage(), 3, "intarocrm.log"); - } - } - } - } - } - - protected function getApiDeliveryTypes($intaroCrm) - { - $crmDeliveryTypes = array(); - - try { - $deliveryTypes = $intaroCrm->deliveryTypesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('deliveryTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('deliveryTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - if (!empty($deliveryTypes)) { - $crmDeliveryTypes[] = array(); - foreach ($deliveryTypes as $dType) { - $crmDeliveryTypes[] = array( - 'id_option' => $dType['code'], - 'name' => $dType['name'], - ); - } - } - - return $crmDeliveryTypes; - - } - - protected function getDeliveryTypes($default_lang, $intaroCrm) - { - $deliveryTypes = array(); - - $carriers = Carrier::getCarriers( - $default_lang, true, false, false, - null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE - ); - - if (!empty($carriers)) { - foreach ($carriers as $carrier) { - $deliveryTypes[] = array( - 'type' => 'select', - 'label' => $carrier['name'], - 'name' => 'INTAROCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiDeliveryTypes($intaroCrm), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $deliveryTypes; - } - - protected function getApiStatuses($intaroCrm) - { - $crmStatusTypes = array(); - - try { - $statusTypes = $intaroCrm->orderStatusesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('statusTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('statusTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - if (!empty($statusTypes)) { - $crmStatusTypes[] = array(); - foreach ($statusTypes as $sType) { - $crmStatusTypes[] = array( - 'id_option' => $sType['code'], - 'name' => $sType['name'] - ); - } - } - - return $crmStatusTypes; - } - - protected function getStatusTypes($default_lang, $intaroCrm) - { - $statusTypes = array(); - $states = OrderState::getOrderStates($default_lang, true); - - if (!empty($states)) { - foreach ($states as $state) { - if ($state['name'] != ' ') { - $statusTypes[] = array( - 'type' => 'select', - 'label' => $state['name'], - 'name' => 'INTAROCRM_API_STATUS[' . $state['id_order_state'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiStatuses($intaroCrm), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - - return $statusTypes; - } - - protected function getApiPaymentTypes($intaroCrm) - { - $crmPaymentTypes = array(); - - try { - $paymentTypes = $intaroCrm->paymentTypesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('paymentTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('paymentTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - if (!empty($paymentTypes)) { - $crmPaymentTypes[] = array(); - foreach ($paymentTypes as $pType) { - $crmPaymentTypes[] = array( - 'id_option' => $pType['code'], - 'name' => $pType['name'] - ); - } - } - - return $crmPaymentTypes; - } - - protected function getPaymentTypes($intaroCrm) - { - $payments = $this->getSystemPaymentModules(); - $paymentTypes = array(); - - if (!empty($payments)) { - foreach ($payments as $payment) { - $paymentTypes[] = array( - 'type' => 'select', - 'label' => $payment['name'], - 'name' => 'INTAROCRM_API_PAYMENT[' . $payment['code'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiPaymentTypes($intaroCrm), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $paymentTypes; - } - - protected function getSystemPaymentModules() - { - $shop_id = Context::getContext()->shop->id; - - /* Get all modules then select only payment ones */ - $modules = Module::getModulesOnDisk(true); - /* - * Hack for knivesland - */ - if (Module::getInstanceByName('advancedcheckout') === false) { - foreach ($modules as $module) { - if ($module->tab == 'payments_gateways') - { - if ($module->id) - { - if (!get_class($module) == 'SimpleXMLElement') - $module->country = array(); - $countries = DB::getInstance()->executeS(' - SELECT id_country - FROM '._DB_PREFIX_.'module_country - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($countries as $country) - $module->country[] = $country['id_country']; - - if (!get_class($module) == 'SimpleXMLElement') - $module->currency = array(); - $currencies = DB::getInstance()->executeS(' - SELECT id_currency - FROM '._DB_PREFIX_.'module_currency - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($currencies as $currency) - $module->currency[] = $currency['id_currency']; - - if (!get_class($module) == 'SimpleXMLElement') - $module->group = array(); - $groups = DB::getInstance()->executeS(' - SELECT id_group - FROM '._DB_PREFIX_.'module_group - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($groups as $group) - $module->group[] = $group['id_group']; - } - else - { - $module->country = null; - $module->currency = null; - $module->group = null; - } - - if ($module->active != 0) { - $this->payment_modules[] = array( - 'id' => $module->id, - 'code' => $module->name, - 'name' => $module->displayName - ); - } - - } - } - } else { - require_once(dirname(__FILE__) . '/../advancedcheckout/classes/Payment.php'); - $modules = Payment::getPaymentMethods(); - foreach ($modules as $module) { - $this->payment_modules[] = array( - 'id' => $module['id_payment'], - 'code' => $module['name'], - 'name' => $module['name'] - ); - } - } - - return $this->payment_modules; - } - - protected function getAddressFields() - { - $addressFields = array(); - $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); - - if (!empty($address)) { - foreach ($address as $idx => $a) { - if (!strpos($a, ':')) { - $addressFields[] = array( - 'type' => 'select', - 'label' => $this->l((string)$a), - 'name' => 'INTAROCRM_API_ADDR[' . $idx . ']', - 'required' => false, - 'options' => array( - 'query' => array( - array( - 'name' => '', - 'id_option' => '' - ), - array( - 'name' => $this->l('FIRST_NAME'), - 'id_option' => 'first_name' - ), - array( - 'name' => $this->l('LAST_NAME'), - 'id_option' => 'last_name' - ), - array( - 'name' => $this->l('PHONE'), - 'id_option' => 'phone' - ), - array( - 'name' => $this->l('EMAIL'), - 'id_option' => 'email' - ), - array( - 'name' => $this->l('ADDRESS'), - 'id_option' => 'address' - ), - array( - 'name' => $this->l('COUNTRY'), - 'id_option' => 'country' - ), - array( - 'name' => $this->l('REGION'), - 'id_option' => 'region' - ), - array( - 'name' => $this->l('CITY'), - 'id_option' => 'city' - ), - array( - 'name' => $this->l('ZIP'), - 'id_option' => 'index' - ), - array( - 'name' => $this->l('STREET'), - 'id_option' => 'street' - ), - array( - 'name' => $this->l('BUILDING'), - 'id_option' => 'building' - ), - array( - 'name' => $this->l('FLAT'), - 'id_option' => 'flat' - ), - array( - 'name' => $this->l('INTERCOMCODE'), - 'id_option' => 'intercomcode' - ), - array( - 'name' => $this->l('FLOOR'), - 'id_option' => 'floor' - ), - array( - 'name' => $this->l('BLOCK'), - 'id_option' => 'block' - ), - array( - 'name' => $this->l('HOUSE'), - 'ID' => 'house' - ) - ), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - - return $addressFields; - } - - public function exportCatalog() - { - global $smarty; - $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); - $id_lang = (int)Configuration::get('PS_LANG_DEFAULT'); - $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); - if ($currency->iso_code == 'RUB') { - $currency->iso_code = 'RUR'; - } - - // Get currencies - $currencies = Currency::getCurrencies(); - - // Get categories - $categories = Category::getCategories($id_lang, true, false); - - // Get products - $sql = 'SELECT count(*) as `total` FROM `'._DB_PREFIX_.'product`'; - $rq = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); - - $items = array(); - $total_products = $rq[0]['total']; - $limit = 100; - $start = 0; - $parts = (int)($total_products/$limit) + 1; - - for ($i = 0; $i < $parts; $i++) { - $products = Product::getProducts($id_lang, $start, $limit, 'name', 'asc'); - foreach ($products AS $product) { - // Check for home category - $category = $product['id_category_default']; - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - $temp_categories = Product::getProductCategories($product['id_product']); - - foreach ($temp_categories AS $category) { - if ($category != Configuration::get('PS_HOME_CATEGORY')) - break; - } - - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - continue; - } - - } - $link = new Link(); - $cover = Image::getCover($product['id_product']); - $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'].'-'.$cover['id_image'], 'large_default'); - if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) { - $picture = rtrim($shop_url,"/") . $picture; - } - $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); - $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); - $items[] = array( - 'id_product' => $product['id_product'], - 'price' => $product['price'], - 'purchase_price' => $product['wholesale_price'], - 'name' => htmlspecialchars(strip_tags($product['name'])), - 'article' => htmlspecialchars($product['reference']), - 'id_category_default' => $category, - 'picture' => $picture, - 'url' => $url - ); - } - - $start += 100; - $limit += 100; - unset($products); - } - - $smarty->assign('currencies', $currencies); - $smarty->assign('currency', $currency->iso_code); - $smarty->assign('categories', $categories); - $smarty->assign('products', $items); - $smarty->assign('shop_name', Configuration::get('PS_SHOP_NAME')); - $smarty->assign('company', Configuration::get('PS_SHOP_NAME')); - $smarty->assign('shop_url', $shop_url . __PS_BASE_URI__); - return $this->display(__FILE__, 'export.tpl'); - } - - public function orderHistory() - { - /* - * get last sync date - */ - $lastSync = Configuration::get('INTAROCRM_LAST_SYNC'); - - $startFrom = ($lastSync === false) ? null : $lastSync; - $endTime = date('Y-m-d H:i:s'); - - $data = array(); - $counter = 0; - - /* - * retrive orders from crm since last update - */ - do { - try { - $this->response = $this->intaroCRM->orderHistory( - $startDate = $startFrom, $endDate = $endTime, - $limit = 250, $offset = $counter - ); - $data = array_merge($data, $this->response); - $counter += 250; - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderHistory: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderHistory: ' . $e->getMessage(), 3, "intarocrm.log"); - } - } while (!empty($response)); - - /* - * store recieved data into shop database - */ - if (!empty($data)) { - $toUpdate = array(); - - /* - * Customer object. Will be used for further updates. - */ - $this->customer = new Customer(); - - $statuses = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_STATUS'))); - $deliveries = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_DELIVERY'))); - $payments = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_PAYMENT'))); - - foreach ($data as $order) { - if (!array_key_exists('externalId', $order)) { - /* - * create customer if not exist - */ - $this->customer->getByEmail($order['customer']['email']); - - if (!array_key_exists('externalId', $order['customer'])) { - if (Validate::isEmail($order['customer']['email'])) { - - if (!$this->customer->id) - { - $this->customer->firstname = $order['customer']['firstName']; - $this->customer->lastname = $order['customer']['lastName']; - $this->customer->email = $order['customer']['email']; - $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - - if($this->customer->add()) { - - /* - * create customer address for delivery data - */ - - $this->customer->getByEmail($order['customer']['email']); - $this->customer_id = $this->customer->id; - - $address = new Address(); - $address->id_customer = $this->customer->id; - $address->id_country = $this->default_country; - $address->lastname = $this->customer->lastname; - $address->firstname = $this->customer->firstname; - $address->alias = 'default'; - $address->postcode = $order['deliveryAddress']['index']; - $address->city = $order['deliveryAddress']['city']; - $address->address1 = $order['deliveryAddress']['text']; - $address->phone = $order['phone']; - $address->phone_mobile = $order['phone']; - - $address->add(); - - /* - * store address record id for handle order data - */ - $addr = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addr[0]['id_address']; - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $this->customer->id; - } - - /* - * collect customer ids for single fix request - */ - array_push( - $this->customerFix, - array( - 'id' => $order['customer']['id'], - 'externalId' => $this->customer_id - ) - ); - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $order['customer']['externalId']; - } - - $delivery = $order['deliveryType']; - $payment = $order['paymentType']; - $state = $order['status']; - - $cart = new Cart(); - $cart->id_currency = $this->default_currency; - $cart->id_lang = $this->default_lang; - $cart->id_customer = $this->customer_id; - $cart->id_address_delivery = (int)$this->address_id; - $cart->id_address_invoice = (int)$this->address_id; - $cart->id_carrier = (int)$deliveries[$delivery]; - - $cart->add(); - - $products = array(); - if(!empty($order['items'])) { - foreach ($order['items'] as $item) { - $product = array(); - $product['id_product'] = (int)$item['offer']['externalId']; - $product['quantity'] = $item['quantity']; - $product['id_address_delivery'] = (int)$this->address_id; - $products[] = $product; - } - } - - $cart->setWsCartRows($products); - $cart->update(); - - /* - * Create order - */ - - $newOrder = new Order(); - $newOrder->id_address_delivery = (int)$this->address_id; - $newOrder->id_address_invoice = (int)$this->address_id; - $newOrder->id_cart = (int)$cart->id; - $newOrder->id_currency = $this->default_currency; - $newOrder->id_lang = $this->default_lang; - $newOrder->id_customer = (int)$this->customer_id; - $newOrder->id_carrier = (int)$deliveries[$delivery]; - $newOrder->payment = $payments[$payment]; - $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) - ? $payments[$payment] - : 'advancedcheckout' - ; - $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; - $newOrder->total_products = $order['summ']; - $newOrder->total_products_wt = $order['summ']; - $newOrder->total_shipping = $order['deliveryCost']; - $newOrder->total_shipping_tax_incl = $order['deliveryCost']; - $newOrder->total_shipping_tax_excl = $order['deliveryCost']; - $newOrder->conversion_rate = 1.000000; - $newOrder->current_state = (int)$statuses[$state]; - $newOrder->delivery_date = $order['deliveryDate']; - $newOrder->date_add = $order['createdAt']; - $newOrder->date_upd = $order['createdAt']; - $newOrder->valid = 1; - $newOrder->secure_key = md5(time()); - - if (isset($order['discount'])) - { - $newOrder->total_discounts = $order['discount']; - } - - $newOrder->add(false, false); - - /* - * collect order ids for single fix request - */ - array_push( - $this->orderFix, - array( - 'id' => $order['id'], - 'externalId' => $newOrder->id - ) - ); - - /* - * Create order details - */ - $product_list = array(); - foreach ($order['items'] as $item) { - $product = new Product((int)$item['offer']['externalId'], false, $this->default_lang); - $qty = $item['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int)$newOrder->id.', - 0, - '. $this->context->shop->id.', - '.(int)$product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int)$product['quantity'].', - '.(int)$product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - - try { - $this->intaroCRM->customerFixExternalIds($this->customerFix); - $this->intaroCRM->orderFixExternalIds($this->orderFix); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('fixExternalId: connection error', 3, "intarocrm.log"); - continue; - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('fixExternalId: ' . $e->getMessage(), 3, "intarocrm.log"); - continue; - } - - } else { - if (!in_array($order['id'], $toUpdate)) - { - /* - * take last order update only - */ - $toUpdate[] = $order['id']; - if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { - $orderToUpdate = new Order((int)$order['externalId']); - - /* - * check status - */ - $stype = $order['status']; - if ($statuses[$stype] != null) { - if ($statuses[$stype] != $orderToUpdate->current_state) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `current_state` = \''.$statuses[$stype].'\' - WHERE `id_order` = '.(int)$order['externalId']); - } - } - - /* - * check delivery type - */ - $dtype = $order['deliveryType']; - if ($deliveries[$dtype] != null) { - if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = '.(int)$order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_carrier` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = \''.$orderToUpdate->id.'\''); - } - } - - /* - * check payment type - */ - $ptype = $order['paymentType']; - if ($payments[$ptype] != null) { - if ($payments[$ptype] != $orderToUpdate->payment) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `payment` = \''.$payments[$ptype].'\' - WHERE `id_order` = '.(int)$order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_payment` - SET `payment_method` = \''.$payments[$ptype].'\' - WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); - - } - } - - /* - * check items - */ - - /* - * Clean deleted - */ - foreach ($order['items'] as $key => $item) { - if (isset($item['deleted']) && $item['deleted'] == true) { - Db::getInstance()->execute(' - DELETE FROM `'._DB_PREFIX_.'order_detail` - WHERE `id_order` = '. $orderToUpdate->id .' - AND `product_id` = '.$item['id']); - - unset($order['items'][$key]); - } - } - - /* - * check quantity - */ - - foreach ($orderToUpdate->getProductsDetail() as $orderItem) { - foreach ($order['items'] as $key => $item) { - if ($item['offer']['externalId'] == $orderItem['product_id']) { - if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_detail` - SET `product_quantity` = '.$item['quantity'].', - `product_quantity_in_stock` = '.$item['quantity'].' - WHERE `id_order_detail` = '.$orderItem['id_order_detail']); - } - - unset($order['items'][$key]); - } - } - } - - /* - * check new items - */ - if (!empty($order['items'])) { - foreach ($order['items'] as $key => $newItem) { - $product = new Product((int)$newItem['offer']['externalId'], false, $this->default_lang); - $qty = $newItem['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int)$orderToUpdate->id.', - 0, - '. $this->context->shop->id.', - '.(int)$product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int)$product['quantity'].', - '.(int)$product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - unset($order['items'][$key]); - } - - /* - * Fix prices & discounts - * Discounts only for whole order - */ - $orderDiscout = null; - $orderTotal = $order['summ']; - - if (isset($order['discount']) && $order['discount'] > 0) { - if ($order['discount'] != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; - } - } - - if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { - $percent = ($order['summ'] * $order['discountPercent'])/100; - if ($percent != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; - } - } - - $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; - - if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `total_discounts` = '.$totalDiscount.', - `total_discounts_tax_incl` = '.$totalDiscount.', - `total_discounts_tax_excl` = '.$totalDiscount.', - `total_paid` = '.$orderTotal.', - `total_paid_tax_incl` = '.$orderTotal.', - `total_paid_tax_excl` = '.$orderTotal.' - WHERE `id_order` = '.(int)$order['externalId']); - } - } - } - } - } - - /* - * Update last sync timestamp - */ - try { - Configuration::updateValue( - 'INTAROCRM_LAST_SYNC', - date_format($this->intaroCRM->getGeneratedAt(), 'Y-m-d H:i:s') - ); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('getLastSync: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('getLastSync: ' . $e->getMessage(), 3, "intarocrm.log"); - } - - return count($data) . " records was synced"; - - } else { - return 'Nothing to sync'; - } - - } -} diff --git a/intarocrm/logo.gif b/intarocrm/logo.gif deleted file mode 100644 index c9abff4af5b30e99274d8d056c86e98e156c5b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1124 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 diff --git a/intarocrm/sync.php b/intarocrm/sync.php deleted file mode 100644 index bab4cc34..00000000 --- a/intarocrm/sync.php +++ /dev/null @@ -1,9 +0,0 @@ -orderHistory(); diff --git a/intarocrm/translations/ru.php b/intarocrm/translations/ru.php deleted file mode 100644 index bc44b2e2..00000000 --- a/intarocrm/translations/ru.php +++ /dev/null @@ -1,36 +0,0 @@ -intarocrm_03c4d9465b9b3a7533d18cacc79c7fe4'] = 'IntaroCRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5e695dc9fe273b7bc074e608113f4662'] = 'Модуль интеграции с IntaroCRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_876f23178c29dc2552c0b48bf23cd9bd'] = 'Вы уверены, что хотите удалить модуль?'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5effd5157947e8ba4a08883f198b2e31'] = 'Неверный адрес CRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_576300f5b6faeb746bb6d034d98e7afd'] = 'Неверный API ключ'; -$_MODULE['<{intarocrm}prestashop>intarocrm_fba05687b61bc936d1a9a92371ba8bcf'] = 'Внимание! Часовой пояс в CRM должен совпадать с часовым поясом в магазине, настроки часового пояса CRM можно задать по адресу:'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c888438d14855d7d96a2724ee9c306bd'] = 'Настройки обновлены'; -$_MODULE['<{intarocrm}prestashop>intarocrm_51af428aa0dcceb5230acb267ecb91c5'] = 'Настройка соединения'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4cbd5dbeeef7392e50358b1bc00dd592'] = 'Адрес CRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_7f775042e08eddee6bbfd8fbe0add4a3'] = 'Ключ авторизации'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c9cc8cce247e49bae79f15173ce97354'] = 'Сохранить'; -$_MODULE['<{intarocrm}prestashop>intarocrm_065ab3a28ca4f16f55f103adc7d0226f'] = 'Способы доставки'; -$_MODULE['<{intarocrm}prestashop>intarocrm_33af8066d3c83110d4bd897f687cedd2'] = 'Статусы заказов'; -$_MODULE['<{intarocrm}prestashop>intarocrm_bab959acc06bb03897b294fbb892be6b'] = 'Способы оплаты'; -$_MODULE['<{intarocrm}prestashop>intarocrm_dd7bf230fde8d4836917806aff6a6b27'] = 'Адрес'; -$_MODULE['<{intarocrm}prestashop>intarocrm_630f6dc397fe74e52d5189e2c80f282b'] = 'Вернуться к списку'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5c1cf6cfec2dad86c8ca5286a0294516'] = 'Имя'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c695cfe527a6fcd680114851b86b7555'] = 'Фамилия'; -$_MODULE['<{intarocrm}prestashop>intarocrm_f9dd946cc89c1f3b41a0edbe0f36931d'] = 'Телефон'; -$_MODULE['<{intarocrm}prestashop>intarocrm_61a649a33f2869e5e35fbb7aff3a80d9'] = 'Email'; -$_MODULE['<{intarocrm}prestashop>intarocrm_2664f03ac6b8bb9eee4287720e407db3'] = 'Адрес'; -$_MODULE['<{intarocrm}prestashop>intarocrm_6ddc09dc456001d9854e9fe670374eb2'] = 'Страна'; -$_MODULE['<{intarocrm}prestashop>intarocrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; -$_MODULE['<{intarocrm}prestashop>intarocrm_859214628431995197c0558f7b5f8ffc'] = 'Город'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4348f938bbddd8475e967ccb47ecb234'] = 'Почтовый индекс'; -$_MODULE['<{intarocrm}prestashop>intarocrm_78fce82336bbbdca7f6da7564b8f9325'] = 'Улица'; -$_MODULE['<{intarocrm}prestashop>intarocrm_71a6834884666147c0334f0c40bc7295'] = 'Дом/Строение'; -$_MODULE['<{intarocrm}prestashop>intarocrm_f88a77e3d68d251c3dc4008c327b5a0c'] = 'Квартира'; -$_MODULE['<{intarocrm}prestashop>intarocrm_d977f846d110fcb7f71c6f97330c9d10'] = 'Код домофона'; -$_MODULE['<{intarocrm}prestashop>intarocrm_56c1e354d36beb85b0d881c5b2e24cbe'] = 'Этаж'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4d34f53389ed7f28ca91fc31ea360a66'] = 'Корпус'; -$_MODULE['<{intarocrm}prestashop>intarocrm_49354b452ec305136a56fe7731834156'] = 'Дом/Строение'; diff --git a/retailcrm/bootstrap.php b/retailcrm/bootstrap.php new file mode 100644 index 00000000..7c0cc34d --- /dev/null +++ b/retailcrm/bootstrap.php @@ -0,0 +1,99 @@ + + * @author Alex Lushpai + */ +class RetailcrmAutoloader +{ + /** + * File extension as a string. Defaults to ".php". + */ + protected static $fileExt = '.php'; + + /** + * The top level directory where recursion will begin. + * + */ + protected static $pathTop; + + /** + * Autoload function for registration with spl_autoload_register + * + * Looks recursively through project directory and loads class files based on + * filename match. + * + * @param string $className + */ + public static function loader($className) + { + $directory = new RecursiveDirectoryIterator(self::$pathTop); + $fileIterator = new RecursiveIteratorIterator($directory); + $filename = $className . self::$fileExt; + + foreach ($fileIterator as $file) { + if (strtolower($file->getFilename()) === strtolower($filename) && $file->isReadable()) { + include_once $file->getPathname(); + } + } + + } + + /** + * Sets the $fileExt property + * + * @param string $fileExt The file extension used for class files. Default is "php". + */ + public static function setFileExt($fileExt) + { + self::$fileExt = $fileExt; + } + + /** + * Sets the $path property + * + * @param string $path The path representing the top level where recursion should + * begin. Defaults to the current directory. + */ + public static function setPath($path) + { + self::$pathTop = $path; + } + +} + +RetailcrmAutoloader::setPath(realpath(dirname(__FILE__))); +RetailcrmAutoloader::setFileExt('.php'); +spl_autoload_register('RetailcrmAutoloader::loader'); diff --git a/retailcrm/config.xml b/retailcrm/config.xml new file mode 100644 index 00000000..52524199 --- /dev/null +++ b/retailcrm/config.xml @@ -0,0 +1,13 @@ + + + retailcrm + + + + + + + 1 + 1 + + diff --git a/retailcrm/config_ru.xml b/retailcrm/config_ru.xml new file mode 100644 index 00000000..7a939044 --- /dev/null +++ b/retailcrm/config_ru.xml @@ -0,0 +1,13 @@ + + + retailcrm + + + + + + + 1 + 1 + + diff --git a/retailcrm/job/export.php b/retailcrm/job/export.php new file mode 100644 index 00000000..d2ca9d5c --- /dev/null +++ b/retailcrm/job/export.php @@ -0,0 +1,147 @@ +getCustomers(); +$orderRecords = $orderInstance->getOrdersWithInformations(); + +$delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY'), true); +$payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT'), true); +$status = json_decode(Configuration::get('RETAILCRM_API_STATUS'), true); + +foreach ($customerRecords as $record) { + $customers[$record['id_customer']] = array( + 'externalId' => $record['id_customer'], + 'firstName' => $record['firstname'], + 'lastname' => $record['lastname'], + 'email' => $record['email'] + ); +} + +unset($customerRecords); + +foreach ($orderRecords as $record) { + + $object = new Order($record['id_order']); + + if (Module::getInstanceByName('advancedcheckout') === false) { + $paymentType = $record['module']; + } else { + $paymentType = $record['payment']; + } + + if ($record['current_state'] == 0) { + $order_status = 'completed'; + } else { + $order_status = array_key_exists($record['current_state'], $status) + ? $status[$record['current_state']] + : 'completed' + ; + } + + $cart = new Cart($object->getCartIdStatic($record['id_order'])); + $addressCollection = $cart->getAddressCollection(); + $address = array_shift($addressCollection); + + if ($address instanceof Address) { + $phone = is_null($address->phone) + ? is_null($address->phone_mobile) ? '' : $address->phone_mobile + : $address->phone + ; + + $postcode = $address->postcode; + $city = $address->city; + $addres_line = sprintf("%s %s", $address->address1, $address->address2); + } + + $order = array( + 'externalId' => $record['id_order'], + 'createdAt' => $record['date_add'], + 'status' => $order_status, + 'firstName' => $record['firstname'], + 'lastName' => $record['lastname'], + 'email' => $record['email'], + ); + + if (isset($postcode)) { + $order['delivery']['address']['postcode'] = $postcode; + } + + if (isset($city)) { + $order['delivery']['address']['city'] = $city; + } + + if (isset($addres_line)) { + $order['delivery']['address']['text'] = $addres_line; + } + + if ($phone) { + $order['phone'] = $phone; + } + + if (array_key_exists($paymentType, $payment)) { + $order['paymentType'] = $payment[$paymentType]; + } + + if (array_key_exists($record['id_carrier'], $delivery)) { + $order['delivery']['code'] = $delivery[$record['id_carrier']]; + } + + if (isset($record['total_shipping_tax_incl']) && (int) $record['total_shipping_tax_incl'] > 0) { + $order['delivery']['cost'] = round($record['total_shipping_tax_incl'], 2); + } + + $products = $object->getProducts(); + + foreach($products as $product) { + $item = array( + 'productId' => $product['product_id'], + 'productName' => $product['product_name'], + 'quantity' => $product['product_quantity'], + 'initialPrice' => round($product['product_price'], 2), + 'purchasePrice' => round($product['purchase_supplier_price'], 2) + ); + + $order['items'][] = $item; + } + + if ($record['id_customer']) { + $order['customer']['externalId'] = $record['id_customer']; + } + + $orders[$record['id_order']] = $order; +} + +unset($orderRecords); + +$customers = array_chunk($customers, 50); + +foreach ($customers as $chunk) { + $api->customersUpload($chunk); + time_nanosleep(0, 200000000); +} + +$orders = array_chunk($orders, 50); + +foreach ($orders as $chunk) { + $api->ordersUpload($chunk); + time_nanosleep(0, 200000000); +} diff --git a/retailcrm/job/icml.php b/retailcrm/job/icml.php new file mode 100644 index 00000000..9cf6da53 --- /dev/null +++ b/retailcrm/job/icml.php @@ -0,0 +1,11 @@ +getData(); + +$icml = new RetailcrmIcml(Configuration::get('PS_SHOP_NAME'), _PS_ROOT_DIR_ . '/retailcrm.xml'); +$icml->generate($data[0], $data[1]); diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php new file mode 100644 index 00000000..07e50c5e --- /dev/null +++ b/retailcrm/job/sync.php @@ -0,0 +1,398 @@ +ordersHistory(new DateTime($startFrom)); + +if ($history->isSuccess() && count($history->orders) > 0) { + + $statuses = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_STATUS'), true))); + $deliveries = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_DELIVERY'), true))); + $payments = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_PAYMENT'), true))); + + foreach ($history->orders as $order) { + + if (!array_key_exists('externalId', $order)) { + + $customer = new Customer(); + $customer->getByEmail($order['customer']['email']); + + if ( + !array_key_exists('externalId', $order['customer']) && + Validate::isEmail($order['customer']['email']) + ) { + if (!$customer->id) + { + $customer->firstname = $order['customer']['firstName']; + $customer->lastname = $order['customer']['lastName']; + $customer->email = $order['customer']['email']; + $customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); + + if($customer->add()) { + $customer->getByEmail($order['customer']['email']); + $customer_id = $customer->id; + + $address = new Address(); + $address->id_customer = $customer->id; + $address->id_country = $default_country; + $address->lastname = $customer->lastname; + $address->firstname = $customer->firstname; + $address->alias = 'default'; + $address->postcode = $customer['address']['index']; + $address->city = $customer['address']['city']; + $address->address1 = $customer['address']['text']; + $address->phone = $customer['phones'][0]['number']; + + $address->add(); + $addr = $customer->getAddresses($default_lang); + $address_id = $addr[0]['id_address']; + } + } else { + $addresses = $customer->getAddresses($default_lang); + $address_id = $addresses[0]['id_address']; + $customer_id = $customer->id; + } + + array_push( + $customerFix, + array( + 'id' => $order['customer']['id'], + 'externalId' => $customer_id + ) + ); + } else { + $addresses = $customer->getAddresses($default_lang); + $address_id = $addresses[0]['id_address']; + $customer_id = $order['customer']['externalId']; + } + + $delivery = $order['delivery']['code']; + $payment = $order['paymentType']; + $state = $order['status']; + + $cart = new Cart(); + $cart->id_currency = $default_currency; + $cart->id_lang = $default_lang; + $cart->id_customer = $customer_id; + $cart->id_address_delivery = (int) $address_id; + $cart->id_address_invoice = (int) $address_id; + $cart->id_carrier = (int) $deliveries[$delivery]; + + $cart->add(); + + $products = array(); + + if(!empty($order['items'])) { + foreach ($order['items'] as $item) { + $product = array(); + $product['id_product'] = (int) $item['offer']['externalId']; + $product['quantity'] = $item['quantity']; + $product['id_address_delivery'] = (int) $address_id; + $products[] = $product; + } + } + + $cart->setWsCartRows($products); + $cart->update(); + + /* + * Create order + */ + $newOrder = new Order(); + $newOrder->id_address_delivery = (int) $address_id; + $newOrder->id_address_invoice = (int) $address_id; + $newOrder->id_cart = (int) $cart->id; + $newOrder->id_currency = $default_currency; + $newOrder->id_lang = $default_lang; + $newOrder->id_customer = (int) $customer_id; + $newOrder->id_carrier = (int) $deliveries[$delivery]; + $newOrder->payment = $payments[$payment]; + $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) + ? $payments[$payment] + : 'advancedcheckout' + ; + $newOrder->total_paid = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_tax_incl = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_tax_excl = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_real = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_products = $order['summ']; + $newOrder->total_products_wt = $order['summ']; + $newOrder->total_shipping = $order['delivery']['cost']; + $newOrder->total_shipping_tax_incl = $order['delivery']['cost']; + $newOrder->total_shipping_tax_excl = $order['delivery']['cost']; + $newOrder->conversion_rate = 1.000000; + $newOrder->current_state = (int) $statuses[$state]; + $newOrder->delivery_date = $order['delivery']['date']; + $newOrder->date_add = $order['createdAt']; + $newOrder->date_upd = $order['createdAt']; + $newOrder->valid = 1; + $newOrder->secure_key = md5(time()); + + if (isset($order['discount'])) + { + $newOrder->total_discounts = $order['discount']; + } + + $newOrder->add(false, false); + + /* + * collect order ids for single fix request + */ + array_push($orderFix, array('id' => $order['id'], 'externalId' => $newOrder->id)); + + /* + * Create order details + */ + $product_list = array(); + foreach ($order['items'] as $item) { + $product = new Product((int) $item['offer']['externalId'], false, $default_lang); + $qty = $item['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $newOrder->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + + $this->api->customersFixExternalIds($customerFix); + $this->api->ordersFixExternalIds($orderFix); + } else { + /* + * take last order update only + */ + if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { + $orderToUpdate = new Order((int) $order['externalId']); + + /* + * check status + */ + $stype = $order['status']; + if ($statuses[$stype] != null) { + if ($statuses[$stype] != $orderToUpdate->current_state) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `current_state` = \''.$statuses[$stype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + } + } + + /* + * check delivery type + */ + $dtype = $order['deliveryType']; + if ($deliveries[$dtype] != null) { + if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_carrier` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = \''.$orderToUpdate->id.'\'' + ); + } + } + + /* + * check payment type + */ + $ptype = $order['paymentType']; + if ($payments[$ptype] != null) { + if ($payments[$ptype] != $orderToUpdate->payment) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `payment` = \''.$payments[$ptype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_payment` + SET `payment_method` = \''.$payments[$ptype].'\' + WHERE `order_reference` = \''.$orderToUpdate->reference.'\'' + ); + + } + } + + /* + * Clean deleted items + */ + foreach ($order['items'] as $key => $item) { + if (isset($item['deleted']) && $item['deleted'] == true) { + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'order_detail` + WHERE `id_order` = '. $orderToUpdate->id .' + AND `product_id` = '.$item['id'] + ); + + unset($order['items'][$key]); + } + } + + /* + * Check items quantity + */ + foreach ($orderToUpdate->getProductsDetail() as $orderItem) { + foreach ($order['items'] as $key => $item) { + if ($item['offer']['externalId'] == $orderItem['product_id']) { + if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_detail` + SET `product_quantity` = '.$item['quantity'].', + `product_quantity_in_stock` = '.$item['quantity'].' + WHERE `id_order_detail` = '.$orderItem['id_order_detail'] + ); + } + + unset($order['items'][$key]); + } + } + } + + /* + * Check new items + */ + if (!empty($order['items'])) { + foreach ($order['items'] as $key => $newItem) { + $product = new Product((int) $newItem['offer']['externalId'], false, $default_lang); + $qty = $newItem['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $orderToUpdate->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + unset($order['items'][$key]); + } + + /* + * Fix prices & discounts + * Discounts only for whole order + */ + $orderDiscout = null; + $orderTotal = $order['summ']; + + if (isset($order['discount']) && $order['discount'] > 0) { + if ($order['discount'] != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; + } + } + + if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { + $percent = ($order['summ'] * $order['discountPercent'])/100; + if ($percent != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; + } + } + + $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; + + if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `total_discounts` = '.$totalDiscount.', + `total_discounts_tax_incl` = '.$totalDiscount.', + `total_discounts_tax_excl` = '.$totalDiscount.', + `total_paid` = '.$orderTotal.', + `total_paid_tax_incl` = '.$orderTotal.', + `total_paid_tax_excl` = '.$orderTotal.' + WHERE `id_order` = '.(int) $order['externalId'] + ); + } + } + } + } + + /* + * Update last sync timestamp + */ + Configuration::updateValue('RETAILCRM_LAST_SYNC', $history->generatedAt); +} else { + return 'Nothing to sync'; +} diff --git a/retailcrm/lib/CurlException.php b/retailcrm/lib/CurlException.php new file mode 100644 index 00000000..2ab00f57 --- /dev/null +++ b/retailcrm/lib/CurlException.php @@ -0,0 +1,5 @@ +client = new RetailcrmHttpClient($url, array('apiKey' => $apiKey)); + $this->siteCode = $site; + } + + /** + * Create a order + * + * @param array $order + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersCreate(array $order, $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + return $this->client->makeRequest("/orders/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'order' => json_encode($order) + ))); + } + + /** + * Edit a order + * + * @param array $order + * @param string $by + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersEdit(array $order, $by = 'externalId', $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($order[$by])) { + throw new InvalidArgumentException(sprintf('Order array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/orders/" . $order[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array( + 'order' => json_encode($order), + 'by' => $by, + )) + ); + } + + /** + * Upload array of the orders + * + * @param array $orders + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersUpload(array $orders, $site = null) + { + if (!sizeof($orders)) { + throw new InvalidArgumentException('Parameter `orders` must contains array of the orders'); + } + + return $this->client->makeRequest("/orders/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'orders' => json_encode($orders), + ))); + } + + /** + * Get order by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/orders/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns a orders history + * + * @param DateTime $startDate (default: null) + * @param DateTime $endDate (default: null) + * @param int $limit (default: 100) + * @param int $offset (default: 0) + * @param bool $skipMyChanges (default: true) + * @return RetailcrmApiResponse + */ + public function ordersHistory( + DateTime $startDate = null, + DateTime $endDate = null, + $limit = 100, + $offset = 0, + $skipMyChanges = true + ) { + $parameters = array(); + + if ($startDate) { + $parameters['startDate'] = $startDate->format('Y-m-d H:i:s'); + } + if ($endDate) { + $parameters['endDate'] = $endDate->format('Y-m-d H:i:s'); + } + if ($limit) { + $parameters['limit'] = (int) $limit; + } + if ($offset) { + $parameters['offset'] = (int) $offset; + } + if ($skipMyChanges) { + $parameters['skipMyChanges'] = (bool) $skipMyChanges; + } + + return $this->client->makeRequest('/orders/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns filtered orders list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns statuses of the orders + * + * @param array $ids (default: array()) + * @param array $externalIds (default: array()) + * @return RetailcrmApiResponse + */ + public function ordersStatuses(array $ids = array(), array $externalIds = array()) + { + $parameters = array(); + + if (sizeof($ids)) { + $parameters['ids'] = $ids; + } + if (sizeof($externalIds)) { + $parameters['externalIds'] = $externalIds; + } + + return $this->client->makeRequest('/orders/statuses', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save order IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function ordersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/orders/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'orders' => json_encode($ids), + )); + } + + /** + * Get orders assembly history + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders/packs/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Create a customer + * + * @param array $customer + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersCreate(array $customer, $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + return $this->client->makeRequest("/customers/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customer' => json_encode($customer) + ))); + } + + /** + * Edit a customer + * + * @param array $customer + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersEdit(array $customer, $by = 'externalId', $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($customer[$by])) { + throw new InvalidArgumentException(sprintf('Customer array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/customers/" . $customer[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite( + $site, + array( + 'customer' => json_encode($customer), + 'by' => $by + ) + ) + ); + } + + /** + * Upload array of the customers + * + * @param array $customers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersUpload(array $customers, $site = null) + { + if (!sizeof($customers)) { + throw new InvalidArgumentException('Parameter `customers` must contains array of the customers'); + } + + return $this->client->makeRequest("/customers/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customers' => json_encode($customers), + ))); + } + + /** + * Get customer by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/customers/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns filtered customers list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function customersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/customers', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save customer IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function customersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/customers/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'customers' => json_encode($ids), + )); + } + + /** + * Get purchace prices & stock balance + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventories(array $filter = array(), $page = null, $limit = null, $site = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/store/inventories', RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, $parameters)); + } + + /** + * Upload store inventories + * + * @param array $offers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventoriesUpload(array $offers, $site = null) + { + if (!sizeof($offers)) { + throw new InvalidArgumentException('Parameter `offers` must contains array of the customers'); + } + + return $this->client->makeRequest( + "/store/inventories/upload", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array('offers' => json_encode($offers))) + ); + } + + /** + * Returns deliveryServices list + * + * @return RetailcrmApiResponse + */ + public function deliveryServicesList() + { + return $this->client->makeRequest('/reference/delivery-services', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns deliveryTypes list + * + * @return RetailcrmApiResponse + */ + public function deliveryTypesList() + { + return $this->client->makeRequest('/reference/delivery-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderMethods list + * + * @return RetailcrmApiResponse + */ + public function orderMethodsList() + { + return $this->client->makeRequest('/reference/order-methods', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderTypes list + * + * @return RetailcrmApiResponse + */ + public function orderTypesList() + { + return $this->client->makeRequest('/reference/order-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentStatuses list + * + * @return RetailcrmApiResponse + */ + public function paymentStatusesList() + { + return $this->client->makeRequest('/reference/payment-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentTypes list + * + * @return RetailcrmApiResponse + */ + public function paymentTypesList() + { + return $this->client->makeRequest('/reference/payment-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns productStatuses list + * + * @return RetailcrmApiResponse + */ + public function productStatusesList() + { + return $this->client->makeRequest('/reference/product-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statusGroups list + * + * @return RetailcrmApiResponse + */ + public function statusGroupsList() + { + return $this->client->makeRequest('/reference/status-groups', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statuses list + * + * @return RetailcrmApiResponse + */ + public function statusesList() + { + return $this->client->makeRequest('/reference/statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns sites list + * + * @return RetailcrmApiResponse + */ + public function sitesList() + { + return $this->client->makeRequest('/reference/sites', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns stores list + * + * @return RetailcrmApiResponse + */ + public function storesList() + { + return $this->client->makeRequest('/reference/stores', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Edit deliveryService + * + * @param array $data delivery service data + * @return RetailcrmApiResponse + */ + public function deliveryServicesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-services/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryService' => json_encode($data) + ) + ); + } + + /** + * Edit deliveryType + * + * @param array $data delivery type data + * @return RetailcrmApiResponse + */ + public function deliveryTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryType' => json_encode($data) + ) + ); + } + + /** + * Edit orderMethod + * + * @param array $data order method data + * @return RetailcrmApiResponse + */ + public function orderMethodsEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-methods/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderMethod' => json_encode($data) + ) + ); + } + + /** + * Edit orderType + * + * @param array $data order type data + * @return RetailcrmApiResponse + */ + public function orderTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderType' => json_encode($data) + ) + ); + } + + /** + * Edit paymentStatus + * + * @param array $data payment status data + * @return RetailcrmApiResponse + */ + public function paymentStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentStatus' => json_encode($data) + ) + ); + } + + /** + * Edit paymentType + * + * @param array $data payment type data + * @return RetailcrmApiResponse + */ + public function paymentTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentType' => json_encode($data) + ) + ); + } + + /** + * Edit productStatus + * + * @param array $data product status data + * @return RetailcrmApiResponse + */ + public function productStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/product-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'productStatus' => json_encode($data) + ) + ); + } + + /** + * Edit order status + * + * @param array $data status data + * @return RetailcrmApiResponse + */ + public function statusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'status' => json_encode($data) + ) + ); + } + + /** + * Edit site + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function sitesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/sites/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'site' => json_encode($data) + ) + ); + } + + /** + * Edit store + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function storesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + if (!isset($data['name'])) { + throw new InvalidArgumentException('Data must contain "name" parameter.'); + } + + return $this->client->makeRequest( + '/reference/stores/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'store' => json_encode($data) + ) + ); + } + + /** + * Update CRM basic statistic + * + * @return RetailcrmApiResponse + */ + public function statisticUpdate() + { + return $this->client->makeRequest('/statistic/update', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Return current site + * + * @return string + */ + public function getSite() + { + return $this->siteCode; + } + + /** + * Set site + * + * @param string $site + * @return void + */ + public function setSite($site) + { + $this->siteCode = $site; + } + + /** + * Check ID parameter + * + * @param string $by + * @return bool + */ + protected function checkIdParameter($by) + { + $allowedForBy = array('externalId', 'id'); + if (!in_array($by, $allowedForBy)) { + throw new InvalidArgumentException(sprintf( + 'Value "%s" for parameter "by" is not valid. Allowed values are %s.', + $by, + implode(', ', $allowedForBy) + )); + } + + return true; + } + + /** + * Fill params by site value + * + * @param string $site + * @param array $params + * @return array + */ + protected function fillSite($site, array $params) + { + if ($site) { + $params['site'] = $site; + } elseif ($this->siteCode) { + $params['site'] = $this->siteCode; + } + + return $params; + } +} diff --git a/retailcrm/lib/RetailcrmApiResponse.php b/retailcrm/lib/RetailcrmApiResponse.php new file mode 100644 index 00000000..bba91db8 --- /dev/null +++ b/retailcrm/lib/RetailcrmApiResponse.php @@ -0,0 +1,122 @@ +statusCode = (int) $statusCode; + + if (!empty($responseBody)) { + $response = json_decode($responseBody, true); + + if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { + throw new InvalidJsonException( + "Invalid JSON in the API response body. Error code #$error", + $error + ); + } + + $this->response = $response; + } + } + + /** + * Return HTTP response status code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * HTTP request was successful + * + * @return bool + */ + public function isSuccessful() + { + return $this->statusCode < 400; + } + + /** + * Allow to access for the property throw class method + * + * @param string $name + * @return mixed + */ + public function __call($name, $arguments) + { + // convert getSomeProperty to someProperty + $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); + + if (!isset($this->response[$propertyName])) { + throw new InvalidArgumentException("Method \"$name\" not found"); + } + + return $this->response[$propertyName]; + } + + /** + * Allow to access for the property throw object property + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!isset($this->response[$name])) { + throw new InvalidArgumentException("Property \"$name\" not found"); + } + + return $this->response[$name]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('This activity not allowed'); + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + throw new BadMethodCallException('This call not allowed'); + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->response[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + if (!isset($this->response[$offset])) { + throw new InvalidArgumentException("Property \"$offset\" not found"); + } + + return $this->response[$offset]; + } +} diff --git a/retailcrm/lib/RetailcrmCatalog.php b/retailcrm/lib/RetailcrmCatalog.php new file mode 100644 index 00000000..a1808b1f --- /dev/null +++ b/retailcrm/lib/RetailcrmCatalog.php @@ -0,0 +1,133 @@ +default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + } + + public function getData() + { + + $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); + + $items = array(); + $categories = array(); + + if ($currency->iso_code == 'RUB') { + $currency->iso_code = 'RUR'; + } + + $currencies = Currency::getCurrencies(); + + $types = Category::getCategories($id_lang, true, false); + foreach ($types AS $category) { + $categories[] = array( + 'id' => $category['id_category'], + 'parentId' => $category['id_parent'], + 'name' => $category['name'] + ); + } + + $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); + + foreach ($products AS $product) { + $category = $product['id_category_default']; + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + $temp_categories = Product::getProductCategories($product['id_product']); + + foreach ($temp_categories AS $category) { + if ($category != Configuration::get('PS_HOME_CATEGORY')) + break; + } + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + continue; + } + } + + $link = new Link(); + $cover = Image::getCover($product['id_product']); + + $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'] . '-' . $cover['id_image'], 'large_default'); + if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) { + $picture = rtrim($shop_url, "/") . $picture; + } + + $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); + $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); + $version = substr(_PS_VERSION_, 0, 3); + + if ($version == "1.3") { + $available_for_order = $product['active'] && $product['quantity']; + $quantity = $product['quantity']; + } else { + $prod = new Product($product['id_product']); + $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); + $quantity = (int) StockAvailable::getQuantityAvailableByProduct($prod->id); + } + + $item = array( + 'id' => $product['id_product'], + 'productId' => $product['id_product'], + 'productActivity' => ($available_for_order) ? 'Y' : 'N', + 'name' => htmlspecialchars(strip_tags($product['name'])), + 'productName' => htmlspecialchars(strip_tags($product['name'])), + 'categoryId' => array($category), + 'picture' => $picture, + 'url' => $url, + 'quantity' => $quantity > 0 ? $quantity : 0 + ); + + if (!empty($product['wholesale_price'])) { + $item['purchasePrice'] = round($product['wholesale_price'], 2); + } + + $item['price'] = !empty($product['rate']) + ? round($product['price'], 2) + (round($product['price'], 2) * $product['rate'] / 100) + : round($product['price'], 2) + ; + + + if (!empty($product['manufacturer_name'])) { + $item['vendor'] = $product['manufacturer_name']; + } + + if (!empty($product['reference'])) { + $item['article'] = htmlspecialchars($product['reference']); + } + + $weight = round($product['weight'], 2); + + if (!empty($weight)) { + $item['weight'] = $weight; + } + + $width = round($product['width'], 2); + $height = round($product['height'], 2); + $depth = round($product['depth'], 2); + + if (!empty($width)) { + if (!empty($height)) { + if (!empty($depth)) { + $item['size'] = implode('x', array($width, $height, $depth)); + } else { + $item['size'] = implode('x', array($width, $height)); + } + } + } + + $items[] = $item; + } + + return array($categories, $items); + } + +} diff --git a/retailcrm/lib/RetailcrmHttpClient.php b/retailcrm/lib/RetailcrmHttpClient.php new file mode 100644 index 00000000..6f74d16a --- /dev/null +++ b/retailcrm/lib/RetailcrmHttpClient.php @@ -0,0 +1,113 @@ +url = $url; + $this->defaultParameters = $defaultParameters; + $this->retry = 0; + } + + /** + * Make HTTP request + * + * @param string $path + * @param string $method (default: 'GET') + * @param array $parameters (default: array()) + * @param int $timeout + * @param bool $verify + * @param bool $debug + * @return RetailcrmApiResponse + */ + public function makeRequest( + $path, + $method, + array $parameters = array(), + $timeout = 30, + $verify = false, + $debug = false + ) { + $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); + if (!in_array($method, $allowedMethods)) { + throw new InvalidArgumentException(sprintf( + 'Method "%s" is not valid. Allowed methods are %s', + $method, + implode(', ', $allowedMethods) + )); + } + + $parameters = array_merge($this->defaultParameters, $parameters); + + $url = $this->url . $path; + + if (self::METHOD_GET === $method && sizeof($parameters)) { + $url .= '?' . http_build_query($parameters, '', '&'); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify); + + if (!$debug) { + curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); + } else { + curl_setopt($ch, CURLOPT_TIMEOUT_MS, (int) $timeout + ($this->retry * 2000)); + } + + if (self::METHOD_POST === $method) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); + } + + $responseBody = curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($errno && in_array($errno, array(6, 7, 28, 34, 35)) && $this->retry < 3) { + $errno = null; + $error = null; + $this->retry += 1; + $this->makeRequest( + $path, + $method, + $parameters, + $timeout, + $verify, + $debug + ); + } + + if ($errno) { + throw new CurlException($error, $errno); + } + + return new RetailcrmApiResponse($statusCode, $responseBody); + } + + public function getRetry() + { + return $this->retry; + } +} diff --git a/retailcrm/lib/RetailcrmIcml.php b/retailcrm/lib/RetailcrmIcml.php new file mode 100644 index 00000000..825d9c2d --- /dev/null +++ b/retailcrm/lib/RetailcrmIcml.php @@ -0,0 +1,137 @@ +shop = $shop; + $this->file = $file; + + $this->properties = array( + 'name', + 'productName', + 'price', + 'purchasePrice', + 'vendor', + 'picture', + 'url', + 'xmlId', + 'productActivity' + ); + + $this->params = array( + 'article' => 'Артикул', + 'color' => 'Цвет', + 'weight' => 'Вес', + 'size' => 'Размер', + 'tax' => 'Наценка' + ); + } + + public function generate($categories, $offers) + { + $string = ' + + + ' . $this->shop . ' + + + + + '; + + $xml = new SimpleXMLElement( + $string, LIBXML_NOENT | LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE + ); + + $this->dd = new DOMDocument(); + $this->dd->preserveWhiteSpace = false; + $this->dd->formatOutput = true; + $this->dd->loadXML($xml->asXML()); + + $this->eCategories = $this->dd + ->getElementsByTagName('categories')->item(0); + $this->eOffers = $this->dd + ->getElementsByTagName('offers')->item(0); + + $this->addCategories($categories); + $this->addOffers($offers); + + $this->dd->saveXML(); + $this->dd->save($this->file); + } + + private function addCategories($categories) + { + foreach ($categories as $category) { + $e = $this->eCategories->appendChild( + $this->dd->createElement( + 'category', $category['name'] + ) + ); + + $e->setAttribute('id', $category['id']); + + if ($category['parentId'] > 0) { + $e->setAttribute('parentId', $category['parentId']); + } + } + } + + private function addOffers($offers) + { + foreach ($offers as $offer) { + + $e = $this->eOffers->appendChild( + $this->dd->createElement('offer') + ); + + $e->setAttribute('id', $offer['id']); + $e->setAttribute('productId', $offer['productId']); + + if (!empty($offer['quantity'])) { + $e->setAttribute('quantity', (int) $offer['quantity']); + } else { + $e->setAttribute('quantity', 0); + } + + foreach ($offer['categoryId'] as $categoryId) { + $e->appendChild( + $this->dd->createElement('categoryId', $categoryId) + ); + } + + $offerKeys = array_keys($offer); + + foreach ($offerKeys as $key) { + if (in_array($key, $this->properties)) { + $e->appendChild( + $this->dd->createElement($key) + )->appendChild( + $this->dd->createTextNode(trim($offer[$key])) + ); + } + + if (in_array($key, array_keys($this->params))) { + $param = $this->dd->createElement('param'); + $param->setAttribute('code', $key); + $param->setAttribute('name', $this->params[$key]); + $param->appendChild( + $this->dd->createTextNode($offer[$key]) + ); + $e->appendChild($param); + } + } + } + } + +} diff --git a/retailcrm/lib/RetailcrmProxy.php b/retailcrm/lib/RetailcrmProxy.php new file mode 100644 index 00000000..f99f7407 --- /dev/null +++ b/retailcrm/lib/RetailcrmProxy.php @@ -0,0 +1,43 @@ +api = new RetailcrmApiClient($url, $key); + $this->log = $log; + } + + public function __call($method, $arguments) + { + try { + $response = call_user_func_array(array($this->api, $method), $arguments); + + if (!$response->isSuccessful()) { + error_log("[$method] " . $response->getErrorMsg() . "\n", 3, $this->log); + if (isset($response['errors'])) { + $error = implode("\n", $response['errors']); + error_log($error . "\n", 3, $this->log); + } + $response = false; + } + + return $response; + } catch (CurlException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } catch (InvalidJsonException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } + } + +} diff --git a/retailcrm/lib/RetailcrmReferences.php b/retailcrm/lib/RetailcrmReferences.php new file mode 100644 index 00000000..1c630484 --- /dev/null +++ b/retailcrm/lib/RetailcrmReferences.php @@ -0,0 +1,215 @@ +api = $client; + $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + } + + public function getDeliveryTypes() + { + $deliveryTypes = array(); + + $carriers = Carrier::getCarriers( + $this->default_lang, + true, + false, + false, + null, + PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE + ); + + if (!empty($carriers)) { + foreach ($carriers as $carrier) { + $deliveryTypes[] = array( + 'type' => 'select', + 'label' => $carrier['name'], + 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiDeliveryTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $deliveryTypes; + } + + public function getStatuses() + { + $statusTypes = array(); + /** + * TODO: state ids duplicates between both arrays, temporary disable return states + * + $states = array_merge( + OrderState::getOrderStates($this->default_lang, true), + OrderReturnState::getOrderReturnStates($this->default_lang, true) + ); + */ + + $states = OrderState::getOrderStates($this->default_lang, true); + + if (!empty($states)) { + foreach ($states as $state) { + if ($state['name'] != ' ') { + /*$key = isset($state['id_order_state']) + ? $state['id_order_state'] + : $state['id_order_return_state'] + ;*/ + $key = $state['id_order_state']; + $statusTypes[] = array( + 'type' => 'select', + 'label' => $state['name'], + 'name' => "RETAILCRM_API_STATUS[$key]", + 'required' => false, + 'options' => array( + 'query' => $this->getApiStatuses(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + } + + return $statusTypes; + } + + public function getPaymentTypes() + { + $payments = $this->getSystemPaymentModules(); + $paymentTypes = array(); + + if (!empty($payments)) { + foreach ($payments as $payment) { + $paymentTypes[] = array( + 'type' => 'select', + 'label' => $payment['name'], + 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiPaymentTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $paymentTypes; + } + + protected function getSystemPaymentModules() + { + $shop_id = Context::getContext()->shop->id; + + /* Get all modules then select only payment ones */ + $modules = Module::getModulesOnDisk(true); + + foreach ($modules as $module) { + if ($module->tab == 'payments_gateways') { + if ($module->id) { + if (!get_class($module) == 'SimpleXMLElement') + $module->country = array(); + $countries = DB::getInstance()->executeS('SELECT id_country FROM ' . _DB_PREFIX_ . 'module_country WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($countries as $country) + $module->country[] = $country['id_country']; + if (!get_class($module) == 'SimpleXMLElement') + $module->currency = array(); + $currencies = DB::getInstance()->executeS('SELECT id_currency FROM ' . _DB_PREFIX_ . 'module_currency WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($currencies as $currency) + $module->currency[] = $currency['id_currency']; + if (!get_class($module) == 'SimpleXMLElement') + $module->group = array(); + $groups = DB::getInstance()->executeS('SELECT id_group FROM ' . _DB_PREFIX_ . 'module_group WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($groups as $group) + $module->group[] = $group['id_group']; + } else { + $module->country = null; + $module->currency = null; + $module->group = null; + } + + if ($module->active != 0) { + $this->payment_modules[] = array( + 'id' => $module->id, + 'code' => $module->name, + 'name' => $module->displayName + ); + } + } + } + + return $this->payment_modules; + } + + protected function getApiDeliveryTypes() + { + $crmDeliveryTypes = array(); + $request = $this->api->deliveryTypesList(); + + if ($request) { + $crmDeliveryTypes[] = array( + 'id_option' => '', + 'name' => '', + ); + foreach ($request->deliveryTypes as $dType) { + $crmDeliveryTypes[] = array( + 'id_option' => $dType['code'], + 'name' => $dType['name'], + ); + } + } + + return $crmDeliveryTypes; + } + + protected function getApiStatuses() + { + $crmStatusTypes = array(); + $request = $this->api->statusesList(); + + if ($request) { + $crmStatusTypes[] = array( + 'id_option' => '', + 'name' => '', + ); + foreach ($request->statuses as $sType) { + $crmStatusTypes[] = array( + 'id_option' => $sType['code'], + 'name' => $sType['name'] + ); + } + } + + return $crmStatusTypes; + } + + protected function getApiPaymentTypes() + { + $crmPaymentTypes = array(); + $request = $this->api->paymentTypesList(); + + if ($request) { + $crmPaymentTypes[] = array( + 'id_option' => '', + 'name' => '', + ); + foreach ($request->paymentTypes as $pType) { + $crmPaymentTypes[] = array( + 'id_option' => $pType['code'], + 'name' => $pType['name'] + ); + } + } + + return $crmPaymentTypes; + } + +} diff --git a/retailcrm/lib/RetailcrmService.php b/retailcrm/lib/RetailcrmService.php new file mode 100644 index 00000000..9a946946 --- /dev/null +++ b/retailcrm/lib/RetailcrmService.php @@ -0,0 +1,42 @@ +=e(WAeQ9s2`B$B+Lx zdGgQMvwyE$`+MWY-Le;+;i`}py{SFirPdGqh{=l|cn z{r~af|IeTQfBpLZ`}hApfBygd`~Tm+|3m{F1|R_W#epr{VP=7cj*#)DMt3pCKp(e< z9m>{$i(RItF-2-@c#v^?ss)GlgBwpWe(^aD&VOM?>c+ zF%Af!6=D9Ioz(^a&}nO^DjE4N?7a-|nw~zs4f@vD@%jCTk$o&REhh$W&yAXoM7IR` zl$gJcA7wJH-K(z!4CRoarx~*1BdF1^Fw?Xpk1Ejw+cPYL^AsTOauE^?U^)&iua}lN zBuN7T+~U8j6YZzGzQ4TJ?yY#54o5d{We(9OlQ65xF)Qhu)*SaKMuRX}pCHl-@ ziMp%`1%KRl%O%D$_~zC3q?ctTp|NohQh{RSOiWdd_MOtff%4hR(i3Z*aNW4kq~>fp z%nASk&T4F;r-SU%&}kT$k-9CndugZ5h{?r<+*Nwki=tnsNAAp3cit<1@_78a5fK|k z|LN0A>!kcK*$;DjnSS*PW7fXttlK{-rw6l}6(MaeTTw@4CQ3p3&D7RBgt#U!Wqc{a z0FoF0z>MX5idSz)R#mv5kgcud%2GTyZGW?8zRf}Y6j!m<2e;C@V>>#?owROmtD(t_ zF3{2l=Kq5;Fh*S?|070j$LBhPMNK$+^%OsqC*D^`5Q&izA)a^3iC zns#W#P=u?nn>q9FH{pQOnLP}E3%9zq`>6kTk6TNVOuxd!Q}%El`*~|EkV3eyj76)w z%nVfybKQM+L?b8<|CyRfre#389u~%9lv1iGczyB3OJAL$_2v&Ttq0jcl^aIlyPKv4 zX^FNjPm)5gy2En2YDKYg*!kQBfsYK?&`4U<^*V0|2gO@qN|`3Xm#qq~>V90o^1`9A zZuG+p!qCey>*=$%>*x%TonPiAj`hb~Z-0|aI%S>vY&A=`GmlmWUHR+e)nl!#KN1w1 z<^frfd=Emll>}cv6>ruPGTGkjCN-u^eYaj)r77M>D>I&pMnwcLhMsI?5id8_79Hr4 zB!&@?Ai?6=p>x*3$2qVLXBzQsLqlXt^qURMweIM|Z^+nKa_2G0=*rF2)$E&H>&$zs zh#9w@c*qxn}+WaGvZ#n2Ctlu8$C9Kmip z`uW^O(|rC4>(5j&GZJsF`c9AI;|nt~NVvKc{5){P>F7e2WNKyB?;X170;Yk>pB-%3 zWM@v>yu^Mxums%(k94>j7K(keLzipMc$NL7=V;Azgos8kp+2lc+0UI{>{j_4P;OnF zf9|+pUSE>8Zs3_>k>uo%CctcuS5dMjkLY!~!S+ZdQf6LGp6DdJ_bZf0KT zw@fsf1FADKvpP+jELusHrlaoZ?YGz#NYojrl9goelF3OC8<1U`>`=_+%VZz|7IxfX z?-j1}EK@0VN_enFp@4@wlQ>(ud&Dpdu1h3CW@F^l!IF4R->+@N`C{{o5DL9~Lm$xw zZv!EAX97elvd&_yr`#o&CtWlmrUgtp{dQK_Tt`Mv&nzwn1wuMtI+_F!u$MdLr_VZx zy7Bt3h3{fjVG#%n8m2JYBZ*C_l(z?2ng%m5O=na))8x0oVRKDip6qW)_x+wn2%?%C zw1x(%;FrFfJmA!5I(Ztrt5+;rra)|mUj=BysU%;xm>C+v+JojVFAzY4AY5ChFi)*5 z?7SilW?S3!cQsaIb0Yv-`#j?p(|qNVhv6aD+3&X4y!J5gQOwpTs@aFXeH zftt6a3X{9kXAUHVkJ(Soz1U`Wbdplf^kpgjfqaH~Bvpbsk64cyw(nB{-n-m;^M(h0 z0(7)d1@2>Keb~{J7Q99hNjh;yOYIOe_Dl_fa$HR<{kVa;URIdL9{|iD` zuZUeT-X1c{M4wi0S0n7rwag-mjk#g{TWFd-jAz1NQ6~2ZlPHOPVn6!tpY84m zgBPNrT$HRNC^e4h+v}PqS!HxI()ny=W}GGhcoYjIPnujAb#mu+M3vPU-JO{-idySK z`QxC-6nAkTz|DYGn^#wL_#rHT(#7o=Seu9ln4lMhr3_eBXlif=vCCzbg@pr3$tvAU zx4#L7j&;0Ss|iU3!7zuTlkde8(}EHOOmXRo9Qi-w&L)Z`Nnzb2j5FUqX-8n2oAq?l z5jRMxAiv&lQcg*asasSHWG(B4P~q|88pZ}5UX-QZN^9!;3QFmr0L#tL9ij_yH_kX# zCMI+9g_gS)6f8lavbN!#jzmCZ1KJ~thR&cHIXnt>R=Lofd0^#SqX2-cRAygC76%t& z-`!_XrGXZveR0!M93|)92Peo{;Qo_5wi+5-aqr3R38a}^uMn%3nW}U+ew>vbntToy zeqH)7qjI61oB|O;<~2V4MO;Yw-L>2HyIZ*D%fo3?tKH^>Su64sGl0(EE9BRtx1E6G zj`Lm%-$=;c^5X{@BGzpS7uPTY{gunW4DJst544!CK^qCY7D+Gzvz^H%UWqiP!Kz#+ ztE6zYnrdm;EN(=AFfo2h+0^}`{ONh;aZ{|=J{+NUHiG`gAaps?X>&nu>sY`TEr5r~H7_tjF zsxf$0tPAiS5*cQKbnao6W_LHmyjk}MInW6HM^p{bbX-Tjh^~K4-=x%=!{D1tUeD!# zlhJfSqWR5DQhW-(?J89wWvuCy1nHMUs!hlz$Md={Ptv-2`cOVs{^YJMN`S4=-4gjJ zX-c%@@~a~%2yZP#X42Jz*w-1d@IhC?y|>PHP^nC!a+*-1gx9meqp93-RF~nT6Uq_AudFb|ATd3Ck^+H&d9kMfCyXzvle`-r zTyC+K`X0Z^Aq8;RovE^TMnl(q*v!^m9A$K}uQO%Q$ZRrNT$jlA^l62i)|E;zwyNU~ z0W}*%-Y2%=(0&DX=BZc)C8N8xjyw>zS+EfCaxMw{&P9uN~jh zSZjtlG3D5p$H5X;o1wL9%~yf;V0d!-RlK<_F_p4;EB%;r=jm@!VFmW|d2K`X!6Bvg zTXszt*O^sY_Rs+ED3FAopS>?aHGQI#L4#Hvf!K2KFPdSkTxuh~EY`$mr(H@o&y z$;s0t@6P%~SzqsS{SWW{Km66!F;>%z035sjPX&nD0z_R4?kmt+4nFC>zSJ8UM8Hp> z_oE$qTJS3yAx_%wzfk= z*Y&=i-gj%_a+5#ufzZQPYv}NBzQGF$8@qu$@N1bIdIr?>-@ebEI3hnMfmJH*y=3Zy zaoScx6@nNm>)n8jEXs~NVvjl&@AI!|Kb72?N=YN%in)tG0Y{E@L^9-CaTOSl`?FlsptKU-bC%T%PFlFk=)d5_y1WoNmz*^01fQ96?_7FpPbF0n`DV z%(pDTsrD6b=l!}utD+TwguvCg>M!L02Yjh?YFxMYcs=&a+v z-lRES1k4Kcg_NBgD9pg7#xjJtj4cnp-4CRmTd1!qeu&@B+SI610Y=b7$>uB_t|u|A zL7VQ`Ms$8SiUdJPui;nybgO4qA;~;OBVDtO_(9vDKm8c z_J%5mE}is99q=kkj;Y%=6o!@g^IVZF2r9+uW$!S)c0^EO_Y`PrYqQwS7No44Z1j=r z{FiZ)<0x3sU*t$D2@xA(Yf@*eWZA*)szK@_EzK254mgRXQ}V$6qY}oa0;}1#I8Y0X zXnOY{wV?rL#LBBFHFlfh8KEY{bHb~H=gOiEn$9?MhE~@`KnLz0wx#QAr${h&#q)zi z+IKi!h&eo{^WF?U3Bn&;>)REwj1Sl91H(Nd?5u%oU_6xoBwX}gzaIVzG7%}IsOlhS z8ez!!0>fWsMhS_l^bS9fHcAk~<%u4;#^k>3m^?EMmuh{zzp|uAcq%^2BOtWg2~@-GHBj!w#0fS#nD0x=l%s ziVVrPa5BE(`xT9xlpHr_>LLtm6y4p>HeaE-Ejv4qS`;DhUxC(`ChbdYk}p27^h%93 zKq2R{QlA#*ZnAi{W7j6X-`Sv=U^rugC$V*r|CZfjVsA%L32|L?q0=(cl{iky{( z=v&v$2Quha5#Hn6#mm97s~LRvA3Rok@gIzw?&C$GiotYHket}}b~1GX0L?^~f(z}s z$kz$S5U+a!cvUh*OocOV6c+I^&JR9!aCRIa`$Z+|{RVfo@hG&^wgKjMsHw@hw|vlA z`6}G7_K`%tbqHzS(au=gwM3obZGe}gvhm7N8%yQsGS&WI6BZ}qgOHF-D;$%^T`0kG z-a0Ch%N=9l%?zwI&&iOaYH9Yq#t?s!?c^lfzrI|t{K0tPxBPyyhe0J!r#XlzE=6?5 z+$(Aing?_e3p-e3`ibkp85%fO6}VfpX!C36c`iqo8>t!w>t+yCzbGazEWIJ-;2J}x zfbg0ZV)O34`y9aK&@t-!4}xnJ&+wb2J`NT0j?A#J_!3cJh9=z%o2#Qq$BNiRR{$8W zaTPFEr2AgVnfl0WW@>keZ{weX6oW8iZ?Urq{rE|@Mnd)wx)5WNOi28VuTC4HjsLJug$(kqkBZfuSHFIAfQ`2MDFR34P}YkGP(Q!X*UV6WfKS z^9~p&Jc8Y%q*P4!RW(!^PX8}ORF9lI=jPxY0;{W3HdJFmo~(_dqvg!chce3CQGWB} z7=?OSborX7$uSPw&NdOr5w^R;cCj(Ub+k8>KCecKV~$n`Nzari7>8a?XdT^=diII3 zQ``{d@WArm2S{3y!7@!^44p;wdcD8O!|ksdd$}d4%c+M~E=foyz`6G$a;%8zW0~4^ z^Hv3=4b>Yd+(I+mzL3RsL!$3qc8UVq!azdW`V`2vQAJ2F9NtIS?}Q%v@vF4XveMmx zk%}$mOmN8LrMrm`f9U15KA+R5vXCGlzd)Xo4i&4q-+Eu)B#0(AZ-*@2>Ht%dl%JY$ zgV#grXtv^>9zAqcq@^B|4nj*;^4`k$La8HAyqilUnNaf4sxf^X3Ruv@IN0p17aNmJ z&wyNPRjXAkJr#Lv`B8i%Wd!OYc7~ToygbV4TFB3-WWqcV?1`o0WAb`gK?Gi`_;hv* zoiSu|l&8FY6WnxqfZqwAAWV5drD*?*KChqCgf|vA9QA+d{}_KtX8RUcaNBS=r>=E^ Q|7rthJk?dLRJIBGe|O%iaR2}S literal 0 HcmV?d00001 diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php new file mode 100644 index 00000000..e47cb4b9 --- /dev/null +++ b/retailcrm/retailcrm.php @@ -0,0 +1,367 @@ +name = 'retailcrm'; + $this->tab = 'export'; + $this->version = '2.0'; + $this->author = 'Retail Driver LCC'; + $this->displayName = $this->l('RetailCRM'); + $this->description = $this->l('Integration module for RetailCRM'); + $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); + $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); + $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); + $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); + $this->ps_versions_compliancy = array('min' => '1.5', 'max' => _PS_VERSION_); + $this->version = substr(_PS_VERSION_, 0, 3); + + if ($this->version == '1.6') { + $this->bootstrap = true; + } + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + $this->api = new RetailcrmProxy($this->apiUrl, $this->apiKey, _PS_ROOT_DIR_ . '/retailcrm.log'); + $this->reference = new RetailcrmReferences($this->api); + } + + parent::__construct(); + } + + function install() + { + return ( + parent::install() && + $this->registerHook('newOrder') && + $this->registerHook('actionOrderStatusPostUpdate') && + $this->registerHook('actionPaymentConfirmation') && + $this->registerHook('actionCustomerAccountAdd') + ); + } + + function uninstall() + { + return parent::uninstall() && + Configuration::deleteByName('RETAILCRM_ADDRESS') && + Configuration::deleteByName('RETAILCRM_API_TOKEN') && + Configuration::deleteByName('RETAILCRM_API_STATUS') && + Configuration::deleteByName('RETAILCRM_API_DELIVERY') && + Configuration::deleteByName('RETAILCRM_LAST_SYNC'); + } + + public function getContent() + { + $output = null; + + $address = Configuration::get('RETAILCRM_ADDRESS'); + $token = Configuration::get('RETAILCRM_API_TOKEN'); + + if (!$address || $address == '') { + $output .= $this->displayError($this->l('Invalid or empty crm address')); + } elseif (!$token || $token == '') { + $output .= $this->displayError($this->l('Invalid or empty crm api token')); + } else { + $output .= $this->displayConfirmation( + $this->l('Timezone settings must be identical to both of your crm and shop') . + " $address/admin/settings#t-main" + ); + } + + if (Tools::isSubmit('submit' . $this->name)) { + $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); + $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); + $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); + $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); + $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); + + if (!$address || empty($address) || !Validate::isGenericName($address)) { + $output .= $this->displayError($this->l('Invalid crm address')); + } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { + $output .= $this->displayError($this->l('Invalid crm api token')); + } else { + Configuration::updateValue('RETAILCRM_ADDRESS', $address); + Configuration::updateValue('RETAILCRM_API_TOKEN', $token); + Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); + Configuration::updateValue('RETAILCRM_API_STATUS', $status); + Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); + $output .= $this->displayConfirmation($this->l('Settings updated')); + } + } + + $this->display(__FILE__, 'retailcrm.tpl'); + + return $output . $this->displayForm(); + } + + public function displayForm() + { + + $this->displayConfirmation($this->l('Settings updated')); + + $default_lang = $this->default_lang; + + /* + * Network connection form + */ + $fields_form[0]['form'] = array( + 'legend' => array( + 'title' => $this->l('Network connection'), + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->l('CRM address'), + 'name' => 'RETAILCRM_ADDRESS', + 'size' => 20, + 'required' => true + ), + array( + 'type' => 'text', + 'label' => $this->l('CRM token'), + 'name' => 'RETAILCRM_API_TOKEN', + 'size' => 20, + 'required' => true + ) + ), + 'submit' => array( + 'title' => $this->l('Save'), + 'class' => 'button' + ) + ); + + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + /* + * Delivery + */ + $fields_form[1]['form'] = array( + 'legend' => array('title' => $this->l('Delivery')), + 'input' => $this->reference->getDeliveryTypes(), + ); + + /* + * Order status + */ + $fields_form[2]['form'] = array( + 'legend' => array('title' => $this->l('Order statuses')), + 'input' => $this->reference->getStatuses(), + ); + + /* + * Payment + */ + $fields_form[3]['form'] = array( + 'legend' => array('title' => $this->l('Payment types')), + 'input' => $this->reference->getPaymentTypes(), + ); + } + + /* + * Diplay forms + */ + + $helper = new HelperForm(); + + $helper->module = $this; + $helper->name_controller = $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name; + + $helper->default_form_language = $default_lang; + $helper->allow_employee_form_lang = $default_lang; + + $helper->title = $this->displayName; + $helper->show_toolbar = true; + $helper->toolbar_scroll = true; + $helper->submit_action = 'submit' . $this->name; + $helper->toolbar_btn = array( + 'save' => + array( + 'desc' => $this->l('Save'), + 'href' => sprintf( + "%s&configure=%s&save%s&token=%s", + AdminController::$currentIndex, + $this->name, + $this->name, + Tools::getAdminTokenLite('AdminModules') + ) + ), + 'back' => array( + 'href' => AdminController::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminModules'), + 'desc' => $this->l('Back to list') + ) + ); + + $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); + $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); + + $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); + if (isset($deliverySettings) && $deliverySettings != '') { + $deliveryTypes = json_decode($deliverySettings); + if ($deliveryTypes) { + foreach ($deliveryTypes as $idx => $delivery) { + $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; + $helper->fields_value[$name] = $delivery; + } + } + } + + $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); + if (isset($statusSettings) && $statusSettings != '') { + $statusTypes = json_decode($statusSettings); + if ($statusTypes) { + foreach ($statusTypes as $idx => $status) { + $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; + $helper->fields_value[$name] = $status; + } + } + } + + $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); + if (isset($paymentSettings) && $paymentSettings != '') { + $paymentTypes = json_decode($paymentSettings); + if ($paymentTypes) { + foreach ($paymentTypes as $idx => $payment) { + $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; + $helper->fields_value[$name] = $payment; + } + } + } + + return $helper->generateForm($fields_form); + } + + public function hookActionCustomerAccountAdd($params) + { + $this->api->customersCreate( + array( + 'externalId' => $params['newCustomer']->id, + 'firstName' => $params['newCustomer']->firstname, + 'lastName' => $params['newCustomer']->lastname, + 'email' => $params['newCustomer']->email, + 'createdAt' => $params['newCustomer']->date_add + ) + ); + } + + public function hookNewOrder($params) + { + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionPaymentConfirmation($params) + { + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'paymentStatus' => 'paid', + 'createdAt' => $params['cart']->date_upd + ) + ); + + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionOrderStatusPostUpdate($params) + { + $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); + $sql = 'SELECT * FROM ' . _DB_PREFIX_ . 'address WHERE id_address=' . (int)$address_id; + $dbaddress = Db::getInstance()->ExecuteS($sql); + $address = $dbaddress[0]; + $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); + $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); + $inCart = $params['cart']->getProducts(); + + if (isset($params['orderStatus'])) { + $this->api->customersEdit( + array( + 'externalId' => $params['cart']->id_customer, + 'lastName' => $params['customer']->lastname, + 'firstName' => $params['customer']->firstname, + 'email' => $params['customer']->email, + 'phones' => array(array('number' => $address['phone'])), + 'createdAt' => $params['customer']->date_add + ) + ); + + $items = array(); + foreach ($inCart as $item) { + $items[] = array( + 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], + 'quantity' => $item['quantity'], + 'productId' => $item['id_product'], + 'productName' => $item['name'], + 'createdAt' => $item['date_add'] + ); + } + + $dTypeKey = $params['cart']->id_carrier; + + if (Module::getInstanceByName('advancedcheckout') === false) { + $pTypeKey = $params['order']->module; + } else { + $pTypeKey = $params['order']->payment; + } + + $this->api->ordersCreate( + array( + 'externalId' => $params['order']->id, + 'orderType' => 'eshop-individual', + 'orderMethod' => 'shopping-cart', + 'status' => 'new', + 'customerId' => $params['cart']->id_customer, + 'firstName' => $params['customer']->firstname, + 'lastName' => $params['customer']->lastname, + 'phone' => $address['phone'], + 'email' => $params['customer']->email, + 'paymentType' => $payment->$pTypeKey, + 'delivery' => array( + 'code' => $delivery->$dTypeKey, + 'cost' => $params['order']->total_shipping, + 'address' => array( + 'city' => $address['city'], + 'index' => $address['postcode'], + 'text' => $address['address1'], + ) + ), + 'discount' => $params['order']->total_discounts, + 'items' => $items, + 'createdAt' => $params['order']->date_add + ) + ); + } + + if (!empty($params['newOrderStatus'])) { + $statuses = OrderState::getOrderStates($this->default_lang); + $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); + foreach ($statuses as $status) { + if ($status['name'] == $params['newOrderStatus']->name) { + $currStatus = $status['id_order_state']; + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'status' => $aStatuses->$currStatus + ) + ); + } + } + } + } +} diff --git a/retailcrm/translations/ru.php b/retailcrm/translations/ru.php new file mode 100644 index 00000000..901ffc1c --- /dev/null +++ b/retailcrm/translations/ru.php @@ -0,0 +1,49 @@ +retailcrm_463dc31aa1a0b6e871b1a9fed8e9860a'] = 'RetailCRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_30de6237576b9a24f6fc599c22a35a4b'] = 'Модуль интеграции с RetailCRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_876f23178c29dc2552c0b48bf23cd9bd'] = 'Вы уверены, что хотите удалить модуль?'; +$_MODULE['<{retailcrm}prestashop>retailcrm_b9b2d9f66d0112f3aae7dbdbd4e22a43'] = 'Некорректный или пустой адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_942010ef43f3fec28741f62a0d9ff29c'] = 'Некорректный или пустой ключ CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_fba05687b61bc936d1a9a92371ba8bcf'] = 'Внимание! Часовой пояс в CRM должен совпадать с часовым поясом в магазине, настроки часового пояса CRM можно задать по адресу:'; +$_MODULE['<{retailcrm}prestashop>retailcrm_5effd5157947e8ba4a08883f198b2e31'] = 'Неверный или пустой адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_576300f5b6faeb746bb6d034d98e7afd'] = 'Неверный или пустой API ключ'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c888438d14855d7d96a2724ee9c306bd'] = 'Настройки обновлены'; +$_MODULE['<{retailcrm}prestashop>retailcrm_51af428aa0dcceb5230acb267ecb91c5'] = 'Настройка соединения'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4cbd5dbeeef7392e50358b1bc00dd592'] = 'URL адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_7f775042e08eddee6bbfd8fbe0add4a3'] = 'API ключ'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c9cc8cce247e49bae79f15173ce97354'] = 'Сохранить'; +$_MODULE['<{retailcrm}prestashop>retailcrm_065ab3a28ca4f16f55f103adc7d0226f'] = 'Способы доставки'; +$_MODULE['<{retailcrm}prestashop>retailcrm_33af8066d3c83110d4bd897f687cedd2'] = 'Статусы заказов'; +$_MODULE['<{retailcrm}prestashop>retailcrm_bab959acc06bb03897b294fbb892be6b'] = 'Способы оплаты'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dd7bf230fde8d4836917806aff6a6b27'] = 'Адрес'; +$_MODULE['<{retailcrm}prestashop>retailcrm_630f6dc397fe74e52d5189e2c80f282b'] = 'Вернуться к списку'; +$_MODULE['<{retailcrm}prestashop>retailcrm_5c1cf6cfec2dad86c8ca5286a0294516'] = 'Имя'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c695cfe527a6fcd680114851b86b7555'] = 'Фамилия'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f9dd946cc89c1f3b41a0edbe0f36931d'] = 'Телефон'; +$_MODULE['<{retailcrm}prestashop>retailcrm_61a649a33f2869e5e35fbb7aff3a80d9'] = 'Email'; +$_MODULE['<{retailcrm}prestashop>retailcrm_2664f03ac6b8bb9eee4287720e407db3'] = 'Адрес'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6ddc09dc456001d9854e9fe670374eb2'] = 'Страна'; +$_MODULE['<{retailcrm}prestashop>retailcrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; +$_MODULE['<{retailcrm}prestashop>retailcrm_859214628431995197c0558f7b5f8ffc'] = 'Город'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4348f938bbddd8475e967ccb47ecb234'] = 'Почтовый индекс'; +$_MODULE['<{retailcrm}prestashop>retailcrm_78fce82336bbbdca7f6da7564b8f9325'] = 'Улица'; +$_MODULE['<{retailcrm}prestashop>retailcrm_71a6834884666147c0334f0c40bc7295'] = 'Дом/Строение'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f88a77e3d68d251c3dc4008c327b5a0c'] = 'Квартира'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d977f846d110fcb7f71c6f97330c9d10'] = 'Код домофона'; +$_MODULE['<{retailcrm}prestashop>retailcrm_56c1e354d36beb85b0d881c5b2e24cbe'] = 'Этаж'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4d34f53389ed7f28ca91fc31ea360a66'] = 'Корпус'; +$_MODULE['<{retailcrm}prestashop>retailcrm_49354b452ec305136a56fe7731834156'] = 'Дом/Строение'; +$_MODULE['<{retailcrm}prestashop>retailcrm_04176f095283bc729f1e3926967e7034'] = 'Имя'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dff4bf10409100d989495c6d5486035e'] = 'Фамилия'; +$_MODULE['<{retailcrm}prestashop>retailcrm_1c76cbfe21c6f44c1d1e59d54f3e4420'] = 'Компания'; +$_MODULE['<{retailcrm}prestashop>retailcrm_1aadcc03a9dbba84a3c5a5cbfde8a162'] = 'ИНН'; +$_MODULE['<{retailcrm}prestashop>retailcrm_93d03fe37ab3c6abc2a19dd8e41543bd'] = 'Адрес строка 1'; +$_MODULE['<{retailcrm}prestashop>retailcrm_22fcffe02ab9eda5b769387122f2ddce'] = 'Адрес строка 2'; +$_MODULE['<{retailcrm}prestashop>retailcrm_8bcdc441379cbf584638b0589a3f9adb'] = 'Почтовый индекс'; +$_MODULE['<{retailcrm}prestashop>retailcrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; +$_MODULE['<{retailcrm}prestashop>retailcrm_57d056ed0984166336b7879c2af3657f'] = 'Город'; +$_MODULE['<{retailcrm}prestashop>retailcrm_bcc254b55c4a1babdf1dcb82c207506b'] = 'Телефон'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f0e1fc6f97d36cb80f29196e2662ffde'] = 'Мобильный телефон';