From 6347f784f7f1b767b570a2f2315eeebabbcc9cd5 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Tue, 20 Jun 2023 14:43:48 +0200 Subject: [PATCH 001/174] [ADD] connector_extension_wordpress: new module --- connector_extension_wordpress/README.rst | 61 +++ connector_extension_wordpress/__init__.py | 1 + connector_extension_wordpress/__manifest__.py | 15 + .../components/__init__.py | 1 + .../components/adapter.py | 82 ++++ .../readme/CONTRIBUTORS.rst | 4 + .../readme/DESCRIPTION.rst | 1 + .../static/description/icon.png | Bin 0 -> 6342 bytes .../static/description/index.html | 419 ++++++++++++++++++ .../odoo/addons/connector_extension_wordpress | 1 + setup/connector_extension_wordpress/setup.py | 6 + 11 files changed, 591 insertions(+) create mode 100644 connector_extension_wordpress/README.rst create mode 100644 connector_extension_wordpress/__init__.py create mode 100644 connector_extension_wordpress/__manifest__.py create mode 100644 connector_extension_wordpress/components/__init__.py create mode 100644 connector_extension_wordpress/components/adapter.py create mode 100644 connector_extension_wordpress/readme/CONTRIBUTORS.rst create mode 100644 connector_extension_wordpress/readme/DESCRIPTION.rst create mode 100644 connector_extension_wordpress/static/description/icon.png create mode 100644 connector_extension_wordpress/static/description/index.html create mode 120000 setup/connector_extension_wordpress/odoo/addons/connector_extension_wordpress create mode 100644 setup/connector_extension_wordpress/setup.py diff --git a/connector_extension_wordpress/README.rst b/connector_extension_wordpress/README.rst new file mode 100644 index 000000000..165d2cdba --- /dev/null +++ b/connector_extension_wordpress/README.rst @@ -0,0 +1,61 @@ +============================= +Connector Extension Wordpress +============================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-nuobit%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/nuobit/odoo-addons/tree/14.0/connector_extension_wordpress + :alt: nuobit/odoo-addons + +|badge1| |badge2| |badge3| + +This module extends the connector extension module to add support for Wordpress + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* NuoBiT Solutions +* SL + +Contributors +~~~~~~~~~~~~ + +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones + +Maintainers +~~~~~~~~~~~ + +This module is part of the `nuobit/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/connector_extension_wordpress/__init__.py b/connector_extension_wordpress/__init__.py new file mode 100644 index 000000000..1377f57f5 --- /dev/null +++ b/connector_extension_wordpress/__init__.py @@ -0,0 +1 @@ +from . import components diff --git a/connector_extension_wordpress/__manifest__.py b/connector_extension_wordpress/__manifest__.py new file mode 100644 index 000000000..d49a162a8 --- /dev/null +++ b/connector_extension_wordpress/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# Copyright NuoBiT Solutions - Eric Antones +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Connector Extension Wordpress", + "summary": "This module extends the connector extension module " + "to add support for Wordpress", + "version": "14.0.1.0.0", + "author": "NuoBiT Solutions, SL", + "license": "LGPL-3", + "category": "Connector", + "website": "https://github.com/nuobit/odoo-addons", + "depends": ["connector_extension"], +} diff --git a/connector_extension_wordpress/components/__init__.py b/connector_extension_wordpress/components/__init__.py new file mode 100644 index 000000000..f502287fe --- /dev/null +++ b/connector_extension_wordpress/components/__init__.py @@ -0,0 +1 @@ +from . import adapter diff --git a/connector_extension_wordpress/components/adapter.py b/connector_extension_wordpress/components/adapter.py new file mode 100644 index 000000000..30a743886 --- /dev/null +++ b/connector_extension_wordpress/components/adapter.py @@ -0,0 +1,82 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import logging + +import requests +from requests.exceptions import ConnectionError as RequestConnectionError + +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import RetryableJobError + +_logger = logging.getLogger(__name__) + + +class ConnectorExtensionWordpressAdapterCRUD(AbstractComponent): + _name = "connector.extension.wordpress.adapter.crud" + _inherit = "connector.extension.adapter.crud" + + def _exec(self, op, resource, *args, **kwargs): + func = getattr(self, "_exec_%s" % op) + return func(resource, *args, **kwargs) + + def _exec_wp_call(self, op, url, *args, **kwargs): + func = getattr(requests, op) + try: + res = func(url, *args, **kwargs) + data = res.json() + if not res.ok: + raise ValidationError(data) + except RequestConnectionError as e: + raise RetryableJobError(_("Error connecting to WordPress: %s") % e) from e + return data + + def _exec_get(self, resource, *args, **kwargs): + url = self.backend_record.url + "/wp-json/wp/v2/" + resource + return self._exec_wp_call( + "get", + url=url, + auth=( + self.backend_record.consumer_key, + self.backend_record.consumer_secret, + ), + ) + + def _exec_post(self, resource, *args, **kwargs): + auth = (self.backend_record.consumer_key, self.backend_record.consumer_secret) + if "wordpress_backend_id" in self.backend_record: + backend = self.backend_record.wordpress_backend_id + auth = (backend.consumer_key, backend.consumer_secret) + data_aux = kwargs.pop("data", {}) + headers = data_aux.pop("headers", {}) + data = data_aux.pop("data", {}) + checksum = False + if data_aux.get("checksum"): + checksum = data_aux.pop("checksum") + url = self.backend_record.url + "/wp-json/wp/v2/" + resource + result = self._exec_wp_call( + "post", url=url, data=data, headers=headers, auth=auth + ) + if checksum: + result["checksum"] = checksum + return result + + def _exec_put(self, resource, *args, **kwargs): + url = self.backend_record.url + "/wp-json/wp/v2/" + resource + return self._exec_wp_call("put", url=url, *args, **kwargs) + + def _exec_delete(self, resource, *args, **kwargs): + raise NotImplementedError() + + def _exec_options(self, resource, *args, **kwargs): + raise NotImplementedError() + + def get_version(self): + settings = self._exec("get", "settings") + if settings.get("title"): + return "Wordpress '%s' connected" % settings.get("title") + else: + raise ValidationError(_("Wordpress not connected")) diff --git a/connector_extension_wordpress/readme/CONTRIBUTORS.rst b/connector_extension_wordpress/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..e468d95a0 --- /dev/null +++ b/connector_extension_wordpress/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones diff --git a/connector_extension_wordpress/readme/DESCRIPTION.rst b/connector_extension_wordpress/readme/DESCRIPTION.rst new file mode 100644 index 000000000..84d9866b2 --- /dev/null +++ b/connector_extension_wordpress/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module extends the connector extension module to add support for Wordpress diff --git a/connector_extension_wordpress/static/description/icon.png b/connector_extension_wordpress/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd641e792c30455187ca30940bc0f329ce8bbb0 GIT binary patch literal 6342 zcmd^^hf`C}*TzHWpfm-MZa|7OjYtjE(4`3pP0Eid3J8V{0s)mKJ<q zp^9|rp$mb~2}po9-@oIXJG(oxcjoS%d!O@s&d!Z9HP*e##KQyt0IurmK_64bp8pyH z9i^|ds>-JfbWVo4P{8GX*QeIfbjl2)kDfIG0ALvZuTgp2ZfK=U();NfY11z-vM>r= zo6RyI007+P`cO@apy}VqnaiVCLL`CEUGVGYE&5WpdhhbZv%|*-Y|2t(4~Cq|y`-Nmm-W zxaTf4+R69rVU1b%qjm?yu*PFgHFYd#J82-D8cpXqO&omwG2*Hd6ZIUiK@+ zNCo8Lg{1^vn^0ZQgz*~*ZR3wsULxnnSBN%7p()3EYs>sX9In)T{*nJ2q*qxXPNhFk z=z=+?4VOOdAF!ZYAVisYzF29g?udLQJtx@=HoAK_Kjx;4SO7>H_v*McB7(}RHMa> z+PNao{Hw&Mjo0P}CBR&l(k@iIeRI@PRH6R9^lR3e?TL?ZHra#GHvKmkeVBHG8nv4{ zz$nHGR7`D$ae@TrcXCSA=$~Yvp@J|bKul>6s-`yT7>JaM5?KcltZ)(ilt^74fqLA{ z1k!bKw(GMV*AOgI*glG_($h!cZgArkEAa1SkSG`0yF8JLWTq^J->2CRaqKH1ZSQt7 z29|+OBS3Rj91K1XL~_9&zn1p z)2Ez)&{9Of1X#b+mpgJ`{gurrlYqKrwrWXTOH{M%kEUhcgSp1J2FK4FF`JS|NfaAA6)?-&1}B`@lI2~kKWK) zhQ|}GQ$j(rNS}9?Yu9}MzWxz*HMwR=u8$RYY6sr2pu3x5Yx*P!Z&c|X zFZcC{+kqJV=XTZH=cMb6)MtgWo%C~XU8TEXDKx9;0hEV*74Z6i8vuzXp zw<8QvI~;n;3@<^G0C#HHf2{N6E~2DO3jw!?w}z?_vV6Q>?kJ>IF-kEc*TtP}k7cVd zvtdPgQ^jWhMXAL$Lqn!_A_IL+!hbY37)n@Sqc)6JwD4)3LP`up1cy^EXzh>B{$ce0 zgX~Iat{I@DM|zU|>9DuD?g}h7zCqV;o1*~3Hr=DYjDq;SG?3HS)(x+l@HAa-@>5wH zhw`oqg>hP$e41h5)>$#qFWq?LGX`dC8ph`RyR&_z&og>psSHzZ=_8<-M4yk+3HK-+ zxqe%Ntx88}49jJazM_Vov;)83cSeeLv@taHOL>zP>~bqdmEyfHl9M%`@ivb|7{I;N zzyHw9P7EH0$ww52RejJv>zvSr8v*iuX@X;(Z~NuUv$D0I_>OkcZWSulBUJjHUN=n| zSI$q@$)`(E;^(|}q|2utYl8}>IcXkPX#{6Z%JnhUBly1B@B}sECm2Y88-QrQZd2n2 zKL=1_&Z87xM=GaycA-Ac*R<^bJk>-^k%lt;DjswC+AM`71*2iG?;!3Bc)I>55v)^C zkt+Uzn&dhv|58XAY6{%ybSiVMl-sATTy=SUADQWD+(@-AVqg@Y+_fBV$LJnIEfujI4B5%4a@8S4M*50Lh7NqKSW>K=U5dW@)Hd{^oR4v% zCM2(rAq7Qe-)R0ko{l@iCHGsxhkCNWby zf&gByp!>=?r1ecWMqz5e-BmOED6n!_1V4<)R!!QNwM!AyGty8>p>ebEzdp*_(kAYA z5*F^g_K}%Rm;V}4Q46qJpU+&3bU10WYg{j`T>lv9{B)J}RHC}yzy9x)wm4ju23yQ& zUNm(i_(ChqD8d7AVUFMw zXmia0A{l#}Sfq!GmHjatiTk$f|OvS0iG>W{p<8cZu^6HX`rMuX?l8<+?WVAW6 z3!MLV*VOFpd&STaeN2qdwU* zk1ni(wdh{`{hLj-hCz&59jVIp~SmgtSQDf!FrPYKIF6_c_NJr zn<-BdXVU}OSE{-No~b(6tG)250`-S%YB9Si@&}{d@FUGqjcNE@SlSdG`}H-#!~M1& z;{E-SKUBb6)KwP1XB|S8MB=F>9k$#1$|^*t%%5zq#(35~S#+TgC^oj&COt~T>axhU0t zQff{8Jt+NH^_pqPzec@Iv#L^r?qs$jdiCY&xOU2pve78Pc{a8y+D;2N0aEJe5d#uL}ZkkYQ&XA;NK5v>r@NUaj=<_V$*Ll@&CF!{LWI zh@|EE!!M(B5qeQ40YHy86TVkX6Te=v4ytV_-JnKl93#Z9clghd^lywoBtgj)4%mxKR<#pH0*hxyHFQNJ zGW`7CtD9C6)ehKni=#!gKj#ZO7L$d_i4nJZhR!z$B(rX9j$$L8X1>~^2By%Dp*IJj z8QiI6*w*|IoF{UpFaD{!PWdOxja{DQq9?BK%2(Xuh#Tv2s_ELIvb@YAd{Af)Lph(9 z>DTXZ`|*!Jnw)?`BzPrdYx(?S2&<(1>1>-f=c}gi8^)=KW973rikh?!-B$fOy@x-Rd+?x= zM(0SbmCz!gY#)CqB9J_^v4K$urOnoj|E||~D>%ndVMwe)ef3BuZH0l!Z&M@fyN}{1 zD;n{juZF|*{lehy$NlM{B`Q0Z18O|&=wX!Nt*rLKfak}ww{ zJ$9BJA3Tq4n~%w3V$0UA(+PgZ#j-35$=_xzuk(w5o2f(WOCu%+h>cg3B*aqaQdfeQ zj@VutKTWtH8{S+}vR3Z`KIQl-h!4tFi1vG-Kuh^Lb0N=LN0+1ZP!WL39=Age)HS_E z8khUbE>xA^59Nmj`B0@u0IR<04wqF@ssF4AP6ZVhslN61xT#8o@ymhOWJ5zkUQN07 zyDEYVZ4#Z$(%wnd04Y_^B_4gjFoKPWgD&OUsj^ezcuXa}E4yjc@xi#az zyRy6>?#h2*VNdNO_jYQ1{@qaYoN7moT}cnd8cmK*&R@SeSYZgIBaJklh!n-3#3dyO z!@*@06=Y8#wl9|Bj3=C0Fi!SfzVz7$Stc4_Q`K2P?2|gT!JIBhc*P&-IkB?Mb5I&% z%BN*TF#vYzIW>)|=X`Chr};G5EZXg?_yvlDC|f%AP!ty{i{{pXQnHm<^|{P$D; z9ZAW#l9Cd2($R5@*5}FeUd#l;N11WwITb1nJSm8r@`#sXHPsuq!3S2&h>U)y=3MjV;j3oWLY>5EOvuruXC*WH2G){378-0tpcMF}1(^PSWUe>XEJN%5 zl|m59cX=GC{^$_E-4Wm1=5|!;Ek&{<4lIOt5M&GMq=+JQdyt?WI#6C!)i!s4;k9T0 z{;`B*>VQ%iU)>Zbhgb4|vd=Wy4>107#gyeqi^+-^2E~0Ja&rFpRb<)oirMj4-KuLg zSo1*y98TZlD<3^A&^bRESh~S*Lzqn0l;JfX-fdjA`M#a!@?b?zWdEr3mIiqS{m2J% z3nWGoQG6+FQ~&gQF-DLGWF}WfwHL(4$EUt(5Jcx#l79K-x~qdu!_gs;XaP0`8m(8a z2J#B{UvEhLT=w9*(6bFWp{9CI=Z&Hh)e}}1hnK6fPlSYqu4H|>g|Erg5fVWl5w&~Kdf{3+V{dCaNhFDg<~sELf1dC($hw|SmSkZ zKD6>nsj6Q+aHEZDHC9{UJxPZ9y{6)F5hg5bm*}ihsxQxj~`xNo%QnaTEJn)f#{CK-H5HYAM7kK zL!XvElM^Y!yC=uSu54Gj zTEgKhtTCOqx1EcIl=VA7`!xLiUj%p*eH??_??@gOJJxVX)#(G`=31lw3whFi2Y7Mq z1bXLvi+~U5E4R{v15H@yQI@=d!V9LD&P!p?0u7L&Rg=D<<*+ zouj?2?aYI{Ac%Gx!r&EkXmmvR`!Xl?06WsGs_Ts8ojW?id!X$>C}@~q>BMfGeGohw zkR}NImw2grp7>W(5s*(iPYn$1*t@i%(W7u#6m}l)%TmD-221>N?VBna!@FO-7!xjM z{`_^-yt<@e?fK$Sqzc7O%3&~A>HB|stQr64jx(U3y+}d}vp(r7c=iB8>t~T7HmYg1qJe4SLo$e62=EZUuFS7UqbSP}M^@%aI7g!ztzj{)_R0x*X6OMLAky)_Sv&%2DNGv zxH}pEr{gEYf&ZF&RJoII9*=yd^~fxKtFc@1f_3}Vqqi8_U?;lC`7etN$3$u0dW+-%7P zQ~iX&gr(5xd1M>3yrzZav9ZLIhbS&|=U$t!9iq*i5vy)(RsBw0TU#?~zdTKUXjyIl z%7Q)Vp}YoU$acz-9y_`%Oig!%TPyC=ie3*Qut3@4V`+A4d<*f%jOx>*bX%#Ao+@wM z;NW0DZKvmp%_oxvFw2#S9r8Sc?wXh}`3gVG`rBKr&jpxwTRQ7WtKY06QQVhs$u$!e zs;Y%~2xwpH*9vxfQ~q#gAwn+P+=YE(L>|P(Fl&H27@?);kUI4FW%LjHZKYGk#f~@3 zXW;a;3+{&c`g+uCR+``$V9)N#RBCk_#RQ(K-PxlQ7Ym;XdCqGn$j%JmAwgtkWKn1} z8^>3&)Q05VbBm+t`9B_${w9F7WfM{Jvawk;HDc*{Sa_Sla|zqX!vbKV%>gB|z6BCc z8_bdnPnzloGP1I)!^5hnC6CLZUU`;nO2NF2)FaAkYhQL$Z58+`p75dj7RKse#Z!uacCm z0@|m~U!QZOdb|V~`ktFK4;lg_ZOCjFXeV4`jGj&bh7Q6BEyN8~yGd*JyzwFbIRaAf z#KG$rvQxWFvqwn`i6jBQ?6o+k+oOC)Gj9ChlgabiScr};b5|opxUYjCZOwmhjTj6W zFzJt_htTuopW4IRiQ}r0L}`w=pE{HN<@(9Hl11P5cHmN6A1F^sg2OWXcw<+q2x>I5 zq9Bu>PBob6#^vrr<|IC)m+zJpFRRcCVsqbspNybriu&!R=H^@RcG#aBGz9RH}ZI=>4 zi(m?IA?Vr$Q7?wN6ZW7H`S?3}K8=$7J5MjWKri=_igw1%J?0~*6e_Ii*1&23dGcF} z&=vaMgF!^veGQ1f$3k?WK5Jaw%==+Bb!tI6zQ68&-dQ3Orl+Tqh#Nt?dBEV_w^wkjY+qJ+X*NCMs%J-Lc4%}pKryM#O)O&9 un*HHVB-AlUN`suyDkKONktc!@Ievk;6wT20MOSqhE{1gM*SZGeqiYU literal 0 HcmV?d00001 diff --git a/connector_extension_wordpress/static/description/index.html b/connector_extension_wordpress/static/description/index.html new file mode 100644 index 000000000..19d9af910 --- /dev/null +++ b/connector_extension_wordpress/static/description/index.html @@ -0,0 +1,419 @@ + + + + + + +Connector Extension Wordpress + + + +
+

Connector Extension Wordpress

+ + +

Beta License: LGPL-3 nuobit/odoo-addons

+

This module extends the connector extension module to add support for Wordpress

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • NuoBiT Solutions
  • +
  • SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the nuobit/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/setup/connector_extension_wordpress/odoo/addons/connector_extension_wordpress b/setup/connector_extension_wordpress/odoo/addons/connector_extension_wordpress new file mode 120000 index 000000000..aa5befdcc --- /dev/null +++ b/setup/connector_extension_wordpress/odoo/addons/connector_extension_wordpress @@ -0,0 +1 @@ +../../../../connector_extension_wordpress \ No newline at end of file diff --git a/setup/connector_extension_wordpress/setup.py b/setup/connector_extension_wordpress/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/connector_extension_wordpress/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 0f212bb7c0290a95e60ae23f2c180460f32c094b Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Tue, 20 Jun 2023 14:43:37 +0200 Subject: [PATCH 002/174] [ADD] connector_extension_woocommerce: new module --- connector_extension_woocommerce/README.rst | 61 +++ connector_extension_woocommerce/__init__.py | 1 + .../__manifest__.py | 15 + .../components/__init__.py | 1 + .../components/adapter.py | 149 +++++++ .../readme/CONTRIBUTORS.rst | 4 + .../readme/DESCRIPTION.rst | 1 + .../static/description/icon.png | Bin 0 -> 6342 bytes .../static/description/index.html | 419 ++++++++++++++++++ .../addons/connector_extension_woocommerce | 1 + .../connector_extension_woocommerce/setup.py | 6 + 11 files changed, 658 insertions(+) create mode 100644 connector_extension_woocommerce/README.rst create mode 100644 connector_extension_woocommerce/__init__.py create mode 100644 connector_extension_woocommerce/__manifest__.py create mode 100644 connector_extension_woocommerce/components/__init__.py create mode 100644 connector_extension_woocommerce/components/adapter.py create mode 100644 connector_extension_woocommerce/readme/CONTRIBUTORS.rst create mode 100644 connector_extension_woocommerce/readme/DESCRIPTION.rst create mode 100644 connector_extension_woocommerce/static/description/icon.png create mode 100644 connector_extension_woocommerce/static/description/index.html create mode 120000 setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce create mode 100644 setup/connector_extension_woocommerce/setup.py diff --git a/connector_extension_woocommerce/README.rst b/connector_extension_woocommerce/README.rst new file mode 100644 index 000000000..cd77ebf50 --- /dev/null +++ b/connector_extension_woocommerce/README.rst @@ -0,0 +1,61 @@ +=============================== +Connector Extension Woocommerce +=============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-nuobit%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/nuobit/odoo-addons/tree/14.0/connector_extension_woocommerce + :alt: nuobit/odoo-addons + +|badge1| |badge2| |badge3| + +This module extends the connector extension module to add support for Woocommerce + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* NuoBiT Solutions +* SL + +Contributors +~~~~~~~~~~~~ + +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones + +Maintainers +~~~~~~~~~~~ + +This module is part of the `nuobit/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/connector_extension_woocommerce/__init__.py b/connector_extension_woocommerce/__init__.py new file mode 100644 index 000000000..1377f57f5 --- /dev/null +++ b/connector_extension_woocommerce/__init__.py @@ -0,0 +1 @@ +from . import components diff --git a/connector_extension_woocommerce/__manifest__.py b/connector_extension_woocommerce/__manifest__.py new file mode 100644 index 000000000..f52cca4a2 --- /dev/null +++ b/connector_extension_woocommerce/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# Copyright NuoBiT Solutions - Eric Antones +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Connector Extension Woocommerce", + "summary": "This module extends the connector extension module " + "to add support for Woocommerce", + "version": "14.0.1.0.0", + "author": "NuoBiT Solutions, SL", + "license": "LGPL-3", + "category": "Connector", + "website": "https://github.com/nuobit/odoo-addons", + "depends": ["connector_extension"], +} diff --git a/connector_extension_woocommerce/components/__init__.py b/connector_extension_woocommerce/components/__init__.py new file mode 100644 index 000000000..f502287fe --- /dev/null +++ b/connector_extension_woocommerce/components/__init__.py @@ -0,0 +1 @@ +from . import adapter diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py new file mode 100644 index 000000000..e47dc7f14 --- /dev/null +++ b/connector_extension_woocommerce/components/adapter.py @@ -0,0 +1,149 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Kilian Niubo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import json +import logging + +from requests.exceptions import ConnectionError as RequestConnectionError + +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import RetryableJobError + +from ...connector_extension.common.tools import trim_domain + +_logger = logging.getLogger(__name__) + + +class ConnectorExtensionWooCommerceAdapterCRUD(AbstractComponent): + _name = "connector.extension.woocommerce.adapter.crud" + _inherit = "connector.extension.adapter.crud" + + def _exec(self, op, resource, *args, **kwargs): + if kwargs.get("domain"): + kwargs["domain"] = trim_domain(kwargs["domain"]) + func = getattr(self, "_exec_%s" % op) + return func(resource, *args, **kwargs) + + def _exec_wcapi_call(self, op, resource, *args, **kwargs): + func = getattr(self.wcapi, op) + try: + res = func(resource, *args, **kwargs) + data = res.json() + if not res.ok and res.status_code == "rest_no_route": + raise ValidationError( + _( + "Error: '%s'. Probably the %s has been removed from Woocommerce. " + "If it's the case, try to remove the binding of the %s." + % (res.get("message"), resource, self.model._name) + ) + ) + elif not res.ok: + raise ValidationError(_("Error: %s") % data) + headers = res.headers + total_items = headers.get("X-WP-Total") or 0 + if total_items: + total_items = int(headers.get("X-WP-Total")) + result = { + "ok": res.ok, + "status_code": res.status_code, + "total_items": total_items, + "data": data, + } + except RequestConnectionError as e: + raise RetryableJobError(_("Error connecting to WooCommerce: %s") % e) from e + except json.JSONDecodeError as e: + raise ValidationError( + _( + "Error decoding json WooCommerce response: " + "%s\nArgs:%s\nKwargs:%s\n" + "URL:%s\nHeaders:%s\nMethod:%s\nBody:%s" + ) + % ( + e, + args, + kwargs, + res.url, + res.request.headers, + res.request.method, + res.text and res.text[:100] + " ...", + ) + ) from e + return result + + def get_total_items(self, resource, domain=None): + filters_values = self._get_search_fields() + real_domain, common_domain = self._extract_domain_clauses( + domain, filters_values + ) + params = self._domain_to_normalized_dict(real_domain) + params["per_page"] = 1 + result = self._exec_wcapi_call("get", resource=resource, params=params) + return result["total_items"] + + def _get_search_fields(self): + return ["modified_after", "offset", "per_page", "page"] + + def _exec_get(self, resource, *args, **kwargs): + if resource == "system_status": + return self._exec_wcapi_call("get", resource=resource, *args, **kwargs) + # WooCommerce has the parameter next on the response headers + # to get the next page but we can't use it because if we use + # the offset, the next page will have the same items as the first page. + # It looks like a bug in WooCommerce API. + domain = [] + if "domain" in kwargs: + domain = kwargs.pop("domain") + search_fields = self._get_search_fields() + real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) + params = self._domain_to_normalized_dict(real_domain) + if "limit" in kwargs: + limit = kwargs.pop("limit") + else: + limit = self.get_total_items(resource, domain) + params["offset"] = ( + kwargs.pop("offset") if "offset" in kwargs and "offset" not in params else 0 + ) + page_size = self.backend_record.page_size + params["per_page"] = page_size if page_size > 0 else 100 + data = [] + while len(data) < limit: + if page_size > limit - len(data): + params["per_page"] = limit - len(data) + res = self._exec_wcapi_call( + "get", resource=resource, params=params, *args, **kwargs + ) + # WooCommerce returns a dict if the response is a single item + if not isinstance(res["data"], list): + res["data"] = [res["data"]] + data += res["data"] + params["offset"] += len(res["data"]) + return self._filter(data, common_domain) + + def _exec_post(self, resource, *args, **kwargs): + res = self._exec_wcapi_call( + "post", + resource, + *args, + **kwargs, + ) + return res["data"] + + def _exec_put(self, resource, *args, **kwargs): + return self._exec_wcapi_call("put", resource, *args, **kwargs) + + def _exec_delete(self, resource, *args, **kwargs): + raise NotImplementedError() + + def _exec_options(self, resource, *args, **kwargs): + raise NotImplementedError() + + def get_version(self): + system_status = self._exec("get", "system_status") + version = False + if system_status: + version = system_status["data"].get("environment").get("version") + return version diff --git a/connector_extension_woocommerce/readme/CONTRIBUTORS.rst b/connector_extension_woocommerce/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..e468d95a0 --- /dev/null +++ b/connector_extension_woocommerce/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones diff --git a/connector_extension_woocommerce/readme/DESCRIPTION.rst b/connector_extension_woocommerce/readme/DESCRIPTION.rst new file mode 100644 index 000000000..cf1445fe3 --- /dev/null +++ b/connector_extension_woocommerce/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module extends the connector extension module to add support for Woocommerce diff --git a/connector_extension_woocommerce/static/description/icon.png b/connector_extension_woocommerce/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd641e792c30455187ca30940bc0f329ce8bbb0 GIT binary patch literal 6342 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 literal 0 HcmV?d00001 diff --git a/connector_extension_woocommerce/static/description/index.html b/connector_extension_woocommerce/static/description/index.html new file mode 100644 index 000000000..e0dcd9354 --- /dev/null +++ b/connector_extension_woocommerce/static/description/index.html @@ -0,0 +1,419 @@ + + + + + + +Connector Extension Woocommerce + + + +
+

Connector Extension Woocommerce

+ + +

Beta License: LGPL-3 nuobit/odoo-addons

+

This module extends the connector extension module to add support for Woocommerce

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • NuoBiT Solutions
  • +
  • SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the nuobit/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce b/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce new file mode 120000 index 000000000..257fd24c0 --- /dev/null +++ b/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce @@ -0,0 +1 @@ +../../../../connector_extension_woocommerce \ No newline at end of file diff --git a/setup/connector_extension_woocommerce/setup.py b/setup/connector_extension_woocommerce/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/connector_extension_woocommerce/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 4fcd7a61f108d1a74acf5c4300e4c51300503d8f Mon Sep 17 00:00:00 2001 From: KNVx Date: Wed, 10 May 2023 13:14:50 +0200 Subject: [PATCH 003/174] [ADD] connector_woocommerce: New Module --- connector_woocommerce/README.rst | 30 ++ connector_woocommerce/__init__.py | 2 + connector_woocommerce/__manifest__.py | 39 +++ connector_woocommerce/components/__init__.py | 7 + connector_woocommerce/components/adapter.py | 25 ++ connector_woocommerce/components/binder.py | 10 + connector_woocommerce/components/core.py | 12 + .../components/export_mapper.py | 14 + connector_woocommerce/components/exporter.py | 32 ++ .../components/import_mapper.py | 14 + connector_woocommerce/components/importer.py | 31 ++ connector_woocommerce/data/ir_cron.xml | 132 ++++++++ connector_woocommerce/data/queue_data.xml | 21 ++ .../data/queue_job_function_data.xml | 147 +++++++++ connector_woocommerce/models/__init__.py | 11 + .../models/backend/__init__.py | 5 + .../models/backend/adapter.py | 15 + .../models/backend/backend.py | 233 +++++++++++++ .../models/backend/backend_account_tax.py | 35 ++ .../backend/backend_delivery_type_provider.py | 41 +++ .../models/backend/backend_payment_mode.py | 43 +++ .../models/backend/backend_tax_class.py | 34 ++ .../models/binding/__init__.py | 1 + .../models/binding/binding.py | 26 ++ .../models/common/__init__.py | 1 + .../models/common/product_attachment.py | 20 ++ connector_woocommerce/models/common/tools.py | 121 +++++++ .../models/product_attribute/__init__.py | 6 + .../models/product_attribute/adapter.py | 30 ++ .../models/product_attribute/binder.py | 16 + .../models/product_attribute/binding.py | 47 +++ .../models/product_attribute/export_mapper.py | 14 + .../models/product_attribute/exporter.py | 35 ++ .../product_attribute/product_attribute.py | 14 + .../product_attribute_value/__init__.py | 6 + .../models/product_attribute_value/adapter.py | 90 +++++ .../models/product_attribute_value/binder.py | 16 + .../models/product_attribute_value/binding.py | 53 +++ .../product_attribute_value/export_mapper.py | 44 +++ .../product_attribute_value/exporter.py | 41 +++ .../product_attribute_value.py | 14 + .../models/product_product/__init__.py | 6 + .../models/product_product/adapter.py | 77 +++++ .../models/product_product/binder.py | 17 + .../models/product_product/binding.py | 67 ++++ .../models/product_product/export_mapper.py | 118 +++++++ .../models/product_product/exporter.py | 75 +++++ .../models/product_product/product.py | 93 ++++++ .../product_public_category/__init__.py | 6 + .../models/product_public_category/adapter.py | 26 ++ .../models/product_public_category/binder.py | 16 + .../models/product_public_category/binding.py | 45 +++ .../product_public_category/export_mapper.py | 23 ++ .../product_public_category/exporter.py | 41 +++ .../product_public_category.py | 14 + .../models/product_template/__init__.py | 6 + .../models/product_template/adapter.py | 81 +++++ .../models/product_template/binder.py | 17 + .../models/product_template/binding.py | 63 ++++ .../models/product_template/export_mapper.py | 202 ++++++++++++ .../models/product_template/exporter.py | 111 +++++++ .../product_template/product_template.py | 119 +++++++ .../models/res_partner/__init__.py | 6 + .../models/res_partner/adapter.py | 11 + .../models/res_partner/binder.py | 19 ++ .../models/res_partner/binding.py | 38 +++ .../models/res_partner/import_mapper.py | 73 ++++ .../models/res_partner/importer.py | 35 ++ .../models/res_partner/res_partner.py | 49 +++ .../models/sale_order/__init__.py | 9 + .../models/sale_order/adapter.py | 175 ++++++++++ .../models/sale_order/binder.py | 16 + .../models/sale_order/binding.py | 71 ++++ .../models/sale_order/export_mapper.py | 22 ++ .../models/sale_order/exporter.py | 35 ++ .../models/sale_order/import_mapper.py | 119 +++++++ .../models/sale_order/importer.py | 129 ++++++++ .../models/sale_order/listener.py | 35 ++ .../models/sale_order/sale_order.py | 106 ++++++ .../models/sale_order_line/__init__.py | 4 + .../models/sale_order_line/binder.py | 38 +++ .../models/sale_order_line/binding.py | 60 ++++ .../models/sale_order_line/import_mapper.py | 108 ++++++ .../models/sale_order_line/sale_order_line.py | 181 ++++++++++ .../security/connector_woocommerce.xml | 13 + .../security/ir.model.access.csv | 27 ++ .../static/description/icon.png | Bin 0 -> 6342 bytes .../views/connector_woocommerce_menu.xml | 67 ++++ .../views/product_attribute.xml | 60 ++++ .../views/product_attribute_value.xml | 47 +++ .../views/product_product.xml | 74 +++++ .../views/product_public_category.xml | 45 +++ .../views/product_template.xml | 76 +++++ .../views/sale_order_view.xml | 87 +++++ .../views/woocommerce_backend_view.xml | 312 ++++++++++++++++++ requirements.txt | 1 + .../odoo/addons/connector_woocommerce | 1 + setup/connector_woocommerce/setup.py | 6 + 98 files changed, 4906 insertions(+) create mode 100644 connector_woocommerce/README.rst create mode 100644 connector_woocommerce/__init__.py create mode 100644 connector_woocommerce/__manifest__.py create mode 100644 connector_woocommerce/components/__init__.py create mode 100644 connector_woocommerce/components/adapter.py create mode 100644 connector_woocommerce/components/binder.py create mode 100644 connector_woocommerce/components/core.py create mode 100644 connector_woocommerce/components/export_mapper.py create mode 100644 connector_woocommerce/components/exporter.py create mode 100644 connector_woocommerce/components/import_mapper.py create mode 100644 connector_woocommerce/components/importer.py create mode 100644 connector_woocommerce/data/ir_cron.xml create mode 100644 connector_woocommerce/data/queue_data.xml create mode 100644 connector_woocommerce/data/queue_job_function_data.xml create mode 100644 connector_woocommerce/models/__init__.py create mode 100644 connector_woocommerce/models/backend/__init__.py create mode 100644 connector_woocommerce/models/backend/adapter.py create mode 100644 connector_woocommerce/models/backend/backend.py create mode 100644 connector_woocommerce/models/backend/backend_account_tax.py create mode 100644 connector_woocommerce/models/backend/backend_delivery_type_provider.py create mode 100644 connector_woocommerce/models/backend/backend_payment_mode.py create mode 100644 connector_woocommerce/models/backend/backend_tax_class.py create mode 100644 connector_woocommerce/models/binding/__init__.py create mode 100644 connector_woocommerce/models/binding/binding.py create mode 100644 connector_woocommerce/models/common/__init__.py create mode 100644 connector_woocommerce/models/common/product_attachment.py create mode 100644 connector_woocommerce/models/common/tools.py create mode 100644 connector_woocommerce/models/product_attribute/__init__.py create mode 100644 connector_woocommerce/models/product_attribute/adapter.py create mode 100644 connector_woocommerce/models/product_attribute/binder.py create mode 100644 connector_woocommerce/models/product_attribute/binding.py create mode 100644 connector_woocommerce/models/product_attribute/export_mapper.py create mode 100644 connector_woocommerce/models/product_attribute/exporter.py create mode 100644 connector_woocommerce/models/product_attribute/product_attribute.py create mode 100644 connector_woocommerce/models/product_attribute_value/__init__.py create mode 100644 connector_woocommerce/models/product_attribute_value/adapter.py create mode 100644 connector_woocommerce/models/product_attribute_value/binder.py create mode 100644 connector_woocommerce/models/product_attribute_value/binding.py create mode 100644 connector_woocommerce/models/product_attribute_value/export_mapper.py create mode 100644 connector_woocommerce/models/product_attribute_value/exporter.py create mode 100644 connector_woocommerce/models/product_attribute_value/product_attribute_value.py create mode 100644 connector_woocommerce/models/product_product/__init__.py create mode 100644 connector_woocommerce/models/product_product/adapter.py create mode 100644 connector_woocommerce/models/product_product/binder.py create mode 100644 connector_woocommerce/models/product_product/binding.py create mode 100644 connector_woocommerce/models/product_product/export_mapper.py create mode 100644 connector_woocommerce/models/product_product/exporter.py create mode 100644 connector_woocommerce/models/product_product/product.py create mode 100644 connector_woocommerce/models/product_public_category/__init__.py create mode 100644 connector_woocommerce/models/product_public_category/adapter.py create mode 100644 connector_woocommerce/models/product_public_category/binder.py create mode 100644 connector_woocommerce/models/product_public_category/binding.py create mode 100644 connector_woocommerce/models/product_public_category/export_mapper.py create mode 100644 connector_woocommerce/models/product_public_category/exporter.py create mode 100644 connector_woocommerce/models/product_public_category/product_public_category.py create mode 100644 connector_woocommerce/models/product_template/__init__.py create mode 100644 connector_woocommerce/models/product_template/adapter.py create mode 100644 connector_woocommerce/models/product_template/binder.py create mode 100644 connector_woocommerce/models/product_template/binding.py create mode 100644 connector_woocommerce/models/product_template/export_mapper.py create mode 100644 connector_woocommerce/models/product_template/exporter.py create mode 100644 connector_woocommerce/models/product_template/product_template.py create mode 100644 connector_woocommerce/models/res_partner/__init__.py create mode 100644 connector_woocommerce/models/res_partner/adapter.py create mode 100644 connector_woocommerce/models/res_partner/binder.py create mode 100644 connector_woocommerce/models/res_partner/binding.py create mode 100644 connector_woocommerce/models/res_partner/import_mapper.py create mode 100644 connector_woocommerce/models/res_partner/importer.py create mode 100644 connector_woocommerce/models/res_partner/res_partner.py create mode 100644 connector_woocommerce/models/sale_order/__init__.py create mode 100644 connector_woocommerce/models/sale_order/adapter.py create mode 100644 connector_woocommerce/models/sale_order/binder.py create mode 100644 connector_woocommerce/models/sale_order/binding.py create mode 100644 connector_woocommerce/models/sale_order/export_mapper.py create mode 100644 connector_woocommerce/models/sale_order/exporter.py create mode 100644 connector_woocommerce/models/sale_order/import_mapper.py create mode 100644 connector_woocommerce/models/sale_order/importer.py create mode 100644 connector_woocommerce/models/sale_order/listener.py create mode 100644 connector_woocommerce/models/sale_order/sale_order.py create mode 100644 connector_woocommerce/models/sale_order_line/__init__.py create mode 100644 connector_woocommerce/models/sale_order_line/binder.py create mode 100644 connector_woocommerce/models/sale_order_line/binding.py create mode 100644 connector_woocommerce/models/sale_order_line/import_mapper.py create mode 100644 connector_woocommerce/models/sale_order_line/sale_order_line.py create mode 100644 connector_woocommerce/security/connector_woocommerce.xml create mode 100644 connector_woocommerce/security/ir.model.access.csv create mode 100644 connector_woocommerce/static/description/icon.png create mode 100644 connector_woocommerce/views/connector_woocommerce_menu.xml create mode 100644 connector_woocommerce/views/product_attribute.xml create mode 100644 connector_woocommerce/views/product_attribute_value.xml create mode 100644 connector_woocommerce/views/product_product.xml create mode 100644 connector_woocommerce/views/product_public_category.xml create mode 100644 connector_woocommerce/views/product_template.xml create mode 100644 connector_woocommerce/views/sale_order_view.xml create mode 100644 connector_woocommerce/views/woocommerce_backend_view.xml create mode 120000 setup/connector_woocommerce/odoo/addons/connector_woocommerce create mode 100644 setup/connector_woocommerce/setup.py diff --git a/connector_woocommerce/README.rst b/connector_woocommerce/README.rst new file mode 100644 index 000000000..1a6e5a6ca --- /dev/null +++ b/connector_woocommerce/README.rst @@ -0,0 +1,30 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +======================= +WooCommerce Connector +======================= + +* WooCommerce connector + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Eric Antones +* Kilian Niubo + + + + diff --git a/connector_woocommerce/__init__.py b/connector_woocommerce/__init__.py new file mode 100644 index 000000000..f24d3e242 --- /dev/null +++ b/connector_woocommerce/__init__.py @@ -0,0 +1,2 @@ +from . import components +from . import models diff --git a/connector_woocommerce/__manifest__.py b/connector_woocommerce/__manifest__.py new file mode 100644 index 000000000..1e38167ff --- /dev/null +++ b/connector_woocommerce/__manifest__.py @@ -0,0 +1,39 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Connector WooCommerce", + "version": "14.0.0.1.0", + "author": "NuoBiT Solutions, S.L.", + "license": "AGPL-3", + "category": "Connector", + "website": "https://github.com/nuobit/odoo-addons", + "external_dependencies": { + "python": [ + "woocommerce", + ], + }, + "depends": [ + "connector_extension_woocommerce", + "website_sale", + "connector_wordpress", + "sale_stock", + ], + "data": [ + "data/ir_cron.xml", + "data/queue_data.xml", + "data/queue_job_function_data.xml", + "security/connector_woocommerce.xml", + "security/ir.model.access.csv", + "views/woocommerce_backend_view.xml", + "views/sale_order_view.xml", + "views/product_template.xml", + "views/product_attribute.xml", + "views/product_attribute_value.xml", + "views/product_public_category.xml", + "views/product_product.xml", + "views/connector_woocommerce_menu.xml", + ], + "installable": True, +} diff --git a/connector_woocommerce/components/__init__.py b/connector_woocommerce/components/__init__.py new file mode 100644 index 000000000..e7d3838df --- /dev/null +++ b/connector_woocommerce/components/__init__.py @@ -0,0 +1,7 @@ +from . import core +from . import adapter +from . import binder +from . import exporter +from . import export_mapper +from . import importer +from . import import_mapper diff --git a/connector_woocommerce/components/adapter.py b/connector_woocommerce/components/adapter.py new file mode 100644 index 000000000..fcb037973 --- /dev/null +++ b/connector_woocommerce/components/adapter.py @@ -0,0 +1,25 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from woocommerce import API as API + +from odoo.addons.component.core import AbstractComponent + + +class ConnectorWooCommerceAdapter(AbstractComponent): + _name = "connector.woocommerce.adapter" + _inherit = [ + "connector.extension.woocommerce.adapter.crud", + "base.woocommerce.connector", + ] + + _description = "WooCommerce Adapter (abstract)" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.wcapi = API( + url=self.backend_record.url, + consumer_key=self.backend_record.consumer_key, + consumer_secret=self.backend_record.consumer_secret, + version="wc/v3", + ) diff --git a/connector_woocommerce/components/binder.py b/connector_woocommerce/components/binder.py new file mode 100644 index 000000000..4b441184a --- /dev/null +++ b/connector_woocommerce/components/binder.py @@ -0,0 +1,10 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.addons.component.core import AbstractComponent + + +class WooCommerceBinder(AbstractComponent): + _name = "woocommerce.binder" + _inherit = ["generic.binder", "base.woocommerce.connector"] + + _default_binding_field = "woocommerce_bind_ids" diff --git a/connector_woocommerce/components/core.py b/connector_woocommerce/components/core.py new file mode 100644 index 000000000..6bef4ed14 --- /dev/null +++ b/connector_woocommerce/components/core.py @@ -0,0 +1,12 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import AbstractComponent + + +class BaseWooCommerceConnector(AbstractComponent): + _name = "base.woocommerce.connector" + _inherit = "base.connector" + _collection = "woocommerce.backend" + + _description = "Base WooCommerce Connector Component" diff --git a/connector_woocommerce/components/export_mapper.py b/connector_woocommerce/components/export_mapper.py new file mode 100644 index 000000000..f0405cb40 --- /dev/null +++ b/connector_woocommerce/components/export_mapper.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import AbstractComponent + + +class WooCommerceExportMapper(AbstractComponent): + _name = "woocommerce.export.mapper" + _inherit = ["connector.extension.export.mapper", "base.woocommerce.connector"] + + +class WooCommerceExportMapChild(AbstractComponent): + _name = "woocommerce.map.child.export" + _inherit = ["connector.extension.map.child.export", "base.woocommerce.connector"] diff --git a/connector_woocommerce/components/exporter.py b/connector_woocommerce/components/exporter.py new file mode 100644 index 000000000..684235655 --- /dev/null +++ b/connector_woocommerce/components/exporter.py @@ -0,0 +1,32 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + + +class WooCommerceRecordDirectExporter(AbstractComponent): + """Base Exporter for WooCommerce""" + + _name = "woocommerce.record.direct.exporter" + _inherit = [ + "generic.record.direct.exporter", + "base.woocommerce.connector", + ] + + +class WooCommerceBatchExporter(AbstractComponent): + """The role of a BatchExporter is to search for a list of + items to export, then it can either export them directly or delay + the export of each item separately. + """ + + _name = "woocommerce.batch.exporter" + _inherit = [ + "generic.record.direct.exporter", + "base.woocommerce.connector", + ] diff --git a/connector_woocommerce/components/import_mapper.py b/connector_woocommerce/components/import_mapper.py new file mode 100644 index 000000000..f27a6e188 --- /dev/null +++ b/connector_woocommerce/components/import_mapper.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import AbstractComponent + + +class WooCommerceImportMapper(AbstractComponent): + _name = "woocommerce.import.mapper" + _inherit = ["connector.extension.import.mapper", "base.woocommerce.connector"] + + +class WooCommerceImportMapChild(AbstractComponent): + _name = "woocommerce.map.child.import" + _inherit = ["connector.extension.map.child.import", "base.woocommerce.connector"] diff --git a/connector_woocommerce/components/importer.py b/connector_woocommerce/components/importer.py new file mode 100644 index 000000000..7c293c2cf --- /dev/null +++ b/connector_woocommerce/components/importer.py @@ -0,0 +1,31 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import logging + +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + + +class WooCommerceDirectImporter(AbstractComponent): + """Base importer for WooCommerce""" + + _name = "woocommerce.record.direct.importer" + _inherit = [ + "generic.record.direct.importer", + "base.woocommerce.connector", + ] + + +class WooCommerceBatchImporter(AbstractComponent): + """The role of a BatchImporter is to search for a list of + items to import, then it can either import them directly or delay + the import of each item separately. + """ + + _name = "woocommerce.batch.importer" + _inherit = [ + "generic.batch.importer", + "base.woocommerce.connector", + ] diff --git a/connector_woocommerce/data/ir_cron.xml b/connector_woocommerce/data/ir_cron.xml new file mode 100644 index 000000000..5eb0fd432 --- /dev/null +++ b/connector_woocommerce/data/ir_cron.xml @@ -0,0 +1,132 @@ + + + + + WooCommerce - Export Product Template + + + + + 1 + days + -1 + + code + model._scheduler_export_product_tmpl() + + + + WooCommerce - Export Product Product + + + + + 1 + days + -1 + + code + model._scheduler_export_products() + + + + WooCommerce - Export Sale Orders + + + + + 1 + days + -1 + + code + model._scheduler_export_sale_orders() + + + + WooCommerce - Import Sale Orders + + + + + 1 + days + -1 + + code + model._scheduler_import_sale_orders() + + + + WooCommerce - Export Product Public Category + + + + + 1 + days + -1 + + code + model._scheduler_export_product_public_category() + + + + WooCommerce - Export Product Attribute + + + + + 1 + days + -1 + + code + model._scheduler_export_product_attribute() + + + + WooCommerce - Export Product Product Attribute Value + + + + + 1 + days + -1 + + code + model._scheduler_export_product_attribute_value() + + diff --git a/connector_woocommerce/data/queue_data.xml b/connector_woocommerce/data/queue_data.xml new file mode 100644 index 000000000..a69687760 --- /dev/null +++ b/connector_woocommerce/data/queue_data.xml @@ -0,0 +1,21 @@ + + + + + woocommerce_export_record + + + + woocommerce_import_batch + + + + woocommerce_import_chunk + + + + woocommerce_import_record + + + diff --git a/connector_woocommerce/data/queue_job_function_data.xml b/connector_woocommerce/data/queue_job_function_data.xml new file mode 100644 index 000000000..77847e55a --- /dev/null +++ b/connector_woocommerce/data/queue_job_function_data.xml @@ -0,0 +1,147 @@ + + + + + + + export_batch + + + + + + export_record + + + + + + + export_record + + + + + + + export_record + + + + + + + export_record + + + + + + + export_record + + + + + + + export_record + + + + + + + import_batch + + + + + + import_chunk + + + + + + import_record + + + + diff --git a/connector_woocommerce/models/__init__.py b/connector_woocommerce/models/__init__.py new file mode 100644 index 000000000..30aaeba1f --- /dev/null +++ b/connector_woocommerce/models/__init__.py @@ -0,0 +1,11 @@ +from . import backend +from . import binding +from . import common +from . import product_public_category +from . import product_attribute +from . import product_attribute_value +from . import product_template +from . import product_product +from . import res_partner +from . import sale_order_line +from . import sale_order diff --git a/connector_woocommerce/models/backend/__init__.py b/connector_woocommerce/models/backend/__init__.py new file mode 100644 index 000000000..65be80dff --- /dev/null +++ b/connector_woocommerce/models/backend/__init__.py @@ -0,0 +1,5 @@ +from . import adapter +from . import backend +from . import backend_account_tax +from . import backend_payment_mode +from . import backend_tax_class diff --git a/connector_woocommerce/models/backend/adapter.py b/connector_woocommerce/models/backend/adapter.py new file mode 100644 index 000000000..c9e6b1401 --- /dev/null +++ b/connector_woocommerce/models/backend/adapter.py @@ -0,0 +1,15 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import logging + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackendAdapter(Component): + _name = "connector.woocommerce.backend.adapter" + _inherit = "connector.woocommerce.adapter" + _description = "WooCommerce Backend Adapter" + + _apply_on = "woocommerce.backend" diff --git a/connector_woocommerce/models/backend/backend.py b/connector_woocommerce/models/backend/backend.py new file mode 100644 index 000000000..3f388d7e7 --- /dev/null +++ b/connector_woocommerce/models/backend/backend.py @@ -0,0 +1,233 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackend(models.Model): + _name = "woocommerce.backend" + _inherit = "connector.extension.backend" + _description = "WooCommerce Backend" + + name = fields.Char( + required=True, + ) + url = fields.Char( + help="WooCommerce URL", + required=True, + ) + consumer_key = fields.Char( + help="WooCommerce Consumer Key", + required=True, + ) + consumer_secret = fields.Char( + required=True, + ) + company_id = fields.Many2one( + comodel_name="res.company", + required=True, + default=lambda self: self.env.company, + ondelete="restrict", + ) + payment_mode_ids = fields.One2many( + comodel_name="woocommerce.backend.payment.mode", + inverse_name="backend_id", + string="Payment Mode", + ) + tax_map_ids = fields.One2many( + comodel_name="woocommerce.backend.account.tax", + inverse_name="backend_id", + string="Tax Mapping", + ) + tax_class_ids = fields.One2many( + comodel_name="woocommerce.backend.tax.class", + inverse_name="backend_id", + string="Tax Class", + ) + shipping_product_id = fields.Many2one( + comodel_name="product.product", + ) + + @api.model + def _lang_get(self): + return self.env["res.lang"].get_installed() + + page_size = fields.Integer( + help="Number of records to fetch at a time. Max: 100", + ) + + @api.constrains("page_size") + def _check_page_size(self): + for rec in self: + if rec.page_size > 100: + raise ValidationError(_("Page size must be less than 100")) + + backend_lang = fields.Selection( + _lang_get, "Language", default=lambda self: self.env.lang + ) + client_order_ref_prefix = fields.Char( + string="Client Order Reference Prefix", + help="Prefix to add to the client order reference", + ) + + wordpress_backend_id = fields.Many2one( + comodel_name="wordpress.backend", + ) + export_product_tmpl_since_date = fields.Datetime( + string="Export Product Templates Since", + ) + export_products_since_date = fields.Datetime( + string="Export Products Since", + ) + export_sale_orders_since_date = fields.Datetime( + string="Export Sale Orders Since", + ) + export_product_public_category_since_date = fields.Datetime( + string="Export Product public category Since", + ) + export_product_attribute_since_date = fields.Datetime( + string="Export Product attributes Since", + ) + export_product_attribute_value_since_date = fields.Datetime( + string="Export Product attribute values Since", + ) + # export_cross_up_sell_products_since_date = fields.Datetime( + # string="Export Cross Up-sell products Since", + # ) + import_sale_order_since_date = fields.Datetime( + string="Import Sale Order Since", + ) + stock_location_ids = fields.Many2many( + string="Locations", + comodel_name="stock.location", + readonly=False, + required=True, + domain="[('usage', 'in', ['internal','view'])]", + ) + + def export_product_tmpl_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string(rec.export_product_tmpl_since_date) + rec.export_product_tmpl_since_date = fields.Datetime.now() + self.env["woocommerce.product.template"].export_product_tmpl_since( + backend_record=rec, since_date=since_date + ) + + def export_products_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string(rec.export_products_since_date) + rec.export_products_since_date = fields.Datetime.now() + self.env["woocommerce.product.product"].export_products_since( + backend_record=rec, since_date=since_date + ) + + def export_sale_orders_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string(rec.export_sale_orders_since_date) + rec.export_sale_orders_since_date = fields.Datetime.now() + self.env["woocommerce.sale.order"].export_sale_orders_since( + backend_record=rec, since_date=since_date + ) + + def export_product_public_category_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string( + rec.export_product_public_category_since_date + ) + rec.export_product_public_category_since_date = fields.Datetime.now() + self.env[ + "woocommerce.product.public.category" + ].export_product_public_category_since( + backend_record=rec, since_date=since_date + ) + + def export_product_attribute_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string( + rec.export_product_attribute_since_date + ) + rec.export_product_attribute_since_date = fields.Datetime.now() + self.env["woocommerce.product.attribute"].export_product_attribute_since( + backend_record=rec, since_date=since_date + ) + + def export_product_attribute_value_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string( + rec.export_product_attribute_value_since_date + ) + rec.export_product_attribute_value_since_date = fields.Datetime.now() + self.env[ + "woocommerce.product.attribute.value" + ].export_product_attribute_value_since( + backend_record=rec, since_date=since_date + ) + + # def export_cross_up_sell_products_since(self): + # self.env.user.company_id = self.company_id + # for rec in self: + # since_date = fields.Datetime.from_string( + # rec.export_product_attribute_value_since_date + # ) + # rec.export_cross_up_sell_products_since_date = fields.Datetime.now() + # self.env[ + # "woocommerce.product.template" + # ].export_cross_up_sell_products_since( + # backend_record=rec, since_date=since_date + # ) + + def import_sale_orders_since(self): + self.env.user.company_id = self.company_id + for rec in self: + since_date = fields.Datetime.from_string(rec.import_sale_order_since_date) + rec.import_sale_order_since_date = fields.Datetime.now() + self.env["woocommerce.sale.order"].import_sale_orders_since( + backend_record=rec, since_date=since_date + ) + + # scheduler + @api.model + def _scheduler_export_products(self): + for backend in self.env[self._name].search([]): + backend.export_products_since() + + # scheduler + @api.model + def _scheduler_export_product_tmpl(self): + for backend in self.env[self._name].search([]): + backend.export_product_tmpl_since() + + @api.model + def _scheduler_export_sale_orders(self): + for backend in self.env[self._name].search([]): + backend.export_sale_orders_since() + + @api.model + def _scheduler_import_sale_orders(self): + for backend in self.env[self._name].search([]): + backend.import_sale_orders_since() + + @api.model + def _scheduler_export_product_public_category(self): + for backend in self.env[self._name].search([]): + backend.export_product_public_category_since() + + @api.model + def _scheduler_export_product_attribute(self): + for backend in self.env[self._name].search([]): + backend.export_product_attribute_since() + + @api.model + def _scheduler_export_product_attribute_value(self): + for backend in self.env[self._name].search([]): + backend.export_product_attribute_value_since() diff --git a/connector_woocommerce/models/backend/backend_account_tax.py b/connector_woocommerce/models/backend/backend_account_tax.py new file mode 100644 index 000000000..ba9c873fe --- /dev/null +++ b/connector_woocommerce/models/backend/backend_account_tax.py @@ -0,0 +1,35 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackendAccountTax(models.Model): + _name = "woocommerce.backend.account.tax" + _description = "WooCommerce Backend Account Tax" + + backend_id = fields.Many2one( + string="Backend id", + comodel_name="woocommerce.backend", + required=True, + ondelete="cascade", + ) + woocommerce_tax_rate_id = fields.Integer( + string="WooCommerce Tax Rate ID", + required=True, + ) + account_tax = fields.Many2one( + comodel_name="account.tax", + required=True, + ) + + _sql_constraints = [ + ( + "tax_map_uniq", + "unique(backend_id, woocommerce_tax_rate_id)", + "A binding already exists with the same (backend, woocommerce_tax_rate_id) ID.", + ), + ] diff --git a/connector_woocommerce/models/backend/backend_delivery_type_provider.py b/connector_woocommerce/models/backend/backend_delivery_type_provider.py new file mode 100644 index 000000000..f9a0413c1 --- /dev/null +++ b/connector_woocommerce/models/backend/backend_delivery_type_provider.py @@ -0,0 +1,41 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackendDeliveryTypeProvider(models.Model): + _name = "woocommerce.backend.delivery.type.provider" + _description = "WooCommerce Backend Delivery Type Provider" + + backend_id = fields.Many2one( + string="Backend id", + comodel_name="woocommerce.backend", + required=True, + ondelete="cascade", + ) + + @api.model + def _get_selection_fields(self): + return self.env["delivery.carrier"].fields_get(["delivery_type"])[ + "delivery_type" + ]["selection"] + + delivery_type = fields.Selection( + selection="_get_selection_fields", + ) + woocommerce_provider = fields.Char( + string="WooCommerce provider name", + required=True, + ) + + _sql_constraints = [ + ( + "tax_map_uniq", + "unique(backend_id, delivery_type)", + "A binding already exists with the same (backend, carrier_id) ID.", + ), + ] diff --git a/connector_woocommerce/models/backend/backend_payment_mode.py b/connector_woocommerce/models/backend/backend_payment_mode.py new file mode 100644 index 000000000..dbe55e9bf --- /dev/null +++ b/connector_woocommerce/models/backend/backend_payment_mode.py @@ -0,0 +1,43 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackendAccountTax(models.Model): + _name = "woocommerce.backend.payment.mode" + _description = "WooCommerce Backend Payment Mode" + + backend_id = fields.Many2one( + string="Backend id", + comodel_name="woocommerce.backend", + required=True, + ondelete="cascade", + ) + company_id = fields.Many2one( + comodel_name="res.company", + required=True, + default=lambda self: self.env.company, + ondelete="restrict", + ) + woocommerce_payment_mode = fields.Char( + string="WooCommerce Payment Mode", + required=True, + ) + payment_mode_id = fields.Many2one( + comodel_name="account.payment.mode", + required=True, + check_company=True, + domain="[('payment_type', '=', 'inbound'), ('company_id', '=', company_id)]", + ) + + _sql_constraints = [ + ( + "tax_map_uniq", + "unique(backend_id, woocommerce_payment_mode)", + "A binding already exists with the same (backend, woocommerce_payment_mode) ID.", + ), + ] diff --git a/connector_woocommerce/models/backend/backend_tax_class.py b/connector_woocommerce/models/backend/backend_tax_class.py new file mode 100644 index 000000000..23cea1176 --- /dev/null +++ b/connector_woocommerce/models/backend/backend_tax_class.py @@ -0,0 +1,34 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class WooCommerceBackendTaxClass(models.Model): + _name = "woocommerce.backend.tax.class" + _description = "WooCommerce Backend Tax Class" + + backend_id = fields.Many2one( + string="Backend id", + comodel_name="woocommerce.backend", + required=True, + ondelete="cascade", + ) + account_tax = fields.Many2one( + comodel_name="account.tax", + required=True, + ) + woocommerce_tax_class = fields.Char( + string="WooCommerce Tax Class", + required=True, + ) + _sql_constraints = [ + ( + "tax_map_uniq", + "unique(backend_id, woocommerce_tax_rate_id)", + "A binding already exists with the same (backend, woocommerce_tax_rate_id) ID.", + ), + ] diff --git a/connector_woocommerce/models/binding/__init__.py b/connector_woocommerce/models/binding/__init__.py new file mode 100644 index 000000000..0fec82e8a --- /dev/null +++ b/connector_woocommerce/models/binding/__init__.py @@ -0,0 +1 @@ +from . import binding diff --git a/connector_woocommerce/models/binding/binding.py b/connector_woocommerce/models/binding/binding.py new file mode 100644 index 000000000..cc5c08055 --- /dev/null +++ b/connector_woocommerce/models/binding/binding.py @@ -0,0 +1,26 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class WoocommerceBinding(models.AbstractModel): + _name = "woocommerce.binding" + _inherit = "connector.extension.external.binding" + _description = "WooCommerce Binding" + + # binding fields + backend_id = fields.Many2one( + comodel_name="woocommerce.backend", + string="WooCommerce Backend", + required=True, + ondelete="restrict", + ) + + _sql_constraints = [ + ( + "woocommerce_internal_uniq", + "unique(backend_id, odoo_id)", + "A binding already exists with the same Internal (Odoo) ID.", + ), + ] diff --git a/connector_woocommerce/models/common/__init__.py b/connector_woocommerce/models/common/__init__.py new file mode 100644 index 000000000..dc578aacd --- /dev/null +++ b/connector_woocommerce/models/common/__init__.py @@ -0,0 +1 @@ +from . import product_attachment diff --git a/connector_woocommerce/models/common/product_attachment.py b/connector_woocommerce/models/common/product_attachment.py new file mode 100644 index 000000000..ecf6fcecd --- /dev/null +++ b/connector_woocommerce/models/common/product_attachment.py @@ -0,0 +1,20 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +# This model is used to store in order attachments from product images +class ProductAttachment(models.TransientModel): + _name = "product.attachment" + _order = "sequence" + + sequence = fields.Integer( + string="Sequence", + ) + + attachment_id = fields.Many2one( + comodel_name="ir.attachment", + string="Attachment", + required=True, + ) diff --git a/connector_woocommerce/models/common/tools.py b/connector_woocommerce/models/common/tools.py new file mode 100644 index 000000000..5ab52ed5b --- /dev/null +++ b/connector_woocommerce/models/common/tools.py @@ -0,0 +1,121 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import datetime +import hashlib +import unicodedata + +from odoo import _ +from odoo.exceptions import ValidationError + + +def list2hash(_list): + _hash = hashlib.sha256() + for e in _list: + if isinstance(e, int): + e9 = str(e) + elif isinstance(e, str): + e9 = e + elif isinstance(e, float): + e9 = str(e) + elif e is None: + e9 = "" + else: + raise Exception("Unexpected type for a key: type %s" % type(e)) + _hash.update(e9.encode("utf8")) + return _hash.hexdigest() + + +def domain_to_normalized_dict(self, domain): + """Convert, if possible, standard Odoo domain to a dictionary. + To do so it is necessary to convert all operators to + equal '=' operator. + """ + res = {} + for elem in domain: + if len(elem) != 3: + raise ValidationError(_("Wrong domain clause format %s") % elem) + field, op, value = elem + if op == "=": + if field in res: + raise ValidationError(_("Duplicated field %s") % field) + res[field] = self._normalize_value(value) + elif op == "!=": + if not isinstance(value, bool): + raise ValidationError( + _("Not equal operation not supported for non boolean fields") + ) + if field in res: + raise ValidationError(_("Duplicated field %s") % field) + res[field] = self._normalize_value(not value) + elif op == "in": + if not isinstance(value, (tuple, list)): + raise ValidationError( + _( + "Operator '%(OPERATOR)s' only supports tuples or lists, not %(TYPES)s" + ) + % { + "OPERATOR": op, + "TYPES": type(value), + } + ) + if field in res: + raise ValidationError(_("Duplicated field %s") % field) + res[field] = self._normalize_value(value) + elif op in (">", ">=", "<", "<="): + if not isinstance(value, (datetime.date, datetime.datetime, int)): + raise ValidationError( + _("Type {} not supported for operator {}").format(type(value), op) + ) + if op in (">", "<"): + adj = 1 + if isinstance(value, (datetime.date, datetime.datetime)): + adj = datetime.timedelta(days=adj) + if op == "<": + op, value = "<=", value - adj + else: + op, value = ">=", value + adj + + res[field] = self._normalize_value(value) + else: + raise ValidationError(_("Operator %s not supported") % op) + + return res + + +def convert_item_to_json(item, ct, namespace): + jitem = {} + for path, func, key, multi in ct: + if key in jitem: + raise ValidationError(_("Key %s already exists") % key) + value = item.xpath(path, namespaces=namespace) + if not value: + jitem[key] = None + else: + if multi: + jitem[key] = func(value) + else: + if len(value) > 1: + raise ValidationError(_("Multiple values found for '%s'") % path) + else: + jitem[key] = func(value[0]) + return jitem + + +def convert_to_json(data, ct, namespace): + res = [] + for d in data: + res.append(convert_item_to_json(d, ct, namespace)) + return res + + +def slugify(value): + if not value: + return None + return ( + unicodedata.normalize("NFKD", value) + .encode("ascii", "ignore") + .decode("ascii") + .lower() + .replace(" ", "") + ) diff --git a/connector_woocommerce/models/product_attribute/__init__.py b/connector_woocommerce/models/product_attribute/__init__.py new file mode 100644 index 000000000..fd2c9b08c --- /dev/null +++ b/connector_woocommerce/models/product_attribute/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import product_attribute diff --git a/connector_woocommerce/models/product_attribute/adapter.py b/connector_woocommerce/models/product_attribute/adapter.py new file mode 100644 index 000000000..c3f2f85d8 --- /dev/null +++ b/connector_woocommerce/models/product_attribute/adapter.py @@ -0,0 +1,30 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeAdapter(Component): + _name = "cwoocommerce.product.attribute.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.product.attribute" + + def read(self, _id): # pylint: disable=W8106 + url_l = ["products/attributes", str(_id)] + return self._exec("get", "/".join(url_l)) + + def search_read(self, domain=None): + return self._exec("get", "products/attributes", domain=domain) + + def create(self, data): # pylint: disable=W8106 + return self._exec("post", "products/attributes", data=data) + + def write(self, external_id, data): # pylint: disable=W8106 + url_l = ["products/attributes", str(external_id[0])] + return self._exec("put", "/".join(url_l), data=data) + + def _get_filters_values(self): + res = super()._get_filters_values() + res.append("slug") + return res diff --git a/connector_woocommerce/models/product_attribute/binder.py b/connector_woocommerce/models/product_attribute/binder.py new file mode 100644 index 000000000..98345911c --- /dev/null +++ b/connector_woocommerce/models/product_attribute/binder.py @@ -0,0 +1,16 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeBinder(Component): + _name = "woocommerce.product.attribute.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.product.attribute" + + external_id = "id" + internal_id = "woocommerce_idattribute" + + external_alt_id = "name" diff --git a/connector_woocommerce/models/product_attribute/binding.py b/connector_woocommerce/models/product_attribute/binding.py new file mode 100644 index 000000000..fac0a4221 --- /dev/null +++ b/connector_woocommerce/models/product_attribute/binding.py @@ -0,0 +1,47 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class WooCommerceProductAttribute(models.Model): + _name = "woocommerce.product.attribute" + _inherit = "woocommerce.binding" + _inherits = {"product.attribute": "odoo_id"} + _description = "WooCommerce Product Attribute Binding" + + odoo_id = fields.Many2one( + comodel_name="product.attribute", + string="Product attribute", + required=True, + ondelete="cascade", + ) + woocommerce_idattribute = fields.Integer( + string="ID Product", + readonly=True, + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idattribute)", + "A binding already exists with the same External (idAttribute) ID.", + ), + ] + + @api.model + def _get_base_domain(self): + return [] + + def export_product_attribute_since(self, backend_record=None, since_date=None): + domain = self._get_base_domain() + if since_date: + domain += [ + ( + "write_date", + ">", + since_date.strftime("%Y-%m-%dT%H:%M:%S"), + ) + ] + self.export_batch(backend_record, domain=domain) + return True diff --git a/connector_woocommerce/models/product_attribute/export_mapper.py b/connector_woocommerce/models/product_attribute/export_mapper.py new file mode 100644 index 000000000..6ef0c886e --- /dev/null +++ b/connector_woocommerce/models/product_attribute/export_mapper.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeExportMapper(Component): + _name = "woocommerce.product.attribute.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.product.attribute" + direct = [ + ("name", "name"), + ] diff --git a/connector_woocommerce/models/product_attribute/exporter.py b/connector_woocommerce/models/product_attribute/exporter.py new file mode 100644 index 000000000..49284141c --- /dev/null +++ b/connector_woocommerce/models/product_attribute/exporter.py @@ -0,0 +1,35 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeBatchDirectExporter(Component): + """Export the WooCommerce Product Attibute. + + For every Product Attibute in the list, execute inmediately. + """ + + _name = "woocommerce.product.attribute.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.product.attribute" + + +class WooCommerceProductAttributeBatchDelayedExporter(Component): + """Export the WooCommerce Product Attibute. + + For every Product Attibute in the list, a delayed job is created. + """ + + _name = "woocommerce.product.attribute.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.product.attribute" + + +class WooCommerceProductAttributeExporter(Component): + _name = "woocommerce.product.attribute.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.product.attribute" diff --git a/connector_woocommerce/models/product_attribute/product_attribute.py b/connector_woocommerce/models/product_attribute/product_attribute.py new file mode 100644 index 000000000..010107bd5 --- /dev/null +++ b/connector_woocommerce/models/product_attribute/product_attribute.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.product.attribute", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) diff --git a/connector_woocommerce/models/product_attribute_value/__init__.py b/connector_woocommerce/models/product_attribute_value/__init__.py new file mode 100644 index 000000000..47eac9b0c --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import product_attribute_value diff --git a/connector_woocommerce/models/product_attribute_value/adapter.py b/connector_woocommerce/models/product_attribute_value/adapter.py new file mode 100644 index 000000000..3d7e443cf --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/adapter.py @@ -0,0 +1,90 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component + + +class ValidatonError: + pass + + +class WooCommerceProductAttributeValueAdapter(Component): + _name = "woocommerce.product.attribute.value" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.product.attribute.value" + + def read(self, external_id): # pylint: disable=W8106 + external_id = self.binder_for().id2dict(external_id, in_field=False) + url = "products/attributes/%s/terms/%s" % ( + external_id["parent_id"], + external_id["id"], + ) + return self._exec("get", url) + + def search_read(self, domain=None): + binder = self.binder_for() + domain_dict = self._domain_to_normalized_dict(domain) + external_id_fields = binder.get_id_fields(in_field=False) + _, common_domain = self._extract_domain_clauses(domain, external_id_fields) + external_id = binder.dict2id(domain_dict, in_field=False) + if external_id: + res = self.read(external_id) + else: + # We have parent id but not the woocommerce attribute value id + if "parent_id" in domain_dict: + _, common_domain = self._extract_domain_clauses( + common_domain, "parent_name" + ) + url = "products/attributes/%s/terms" % domain_dict["parent_id"] + res = self._exec("get", url, domain=common_domain) + if res: + res[0]["parent_id"] = domain_dict["parent_id"] + elif "parent_name" in domain_dict: + partner_name, common_domain = self._extract_domain_clauses( + common_domain, ["parent_name"] + ) + url = "products/attributes" + res = self._exec( + "get", url, domain=[("name", "=", domain_dict["parent_name"])] + ) + if not res: + return [] + if len(res) != 1: + raise ValidationError(_("More than one product parent found")) + parent_id = res[0]["id"] + url = "products/attributes/%s/terms" % parent_id + res = self._exec("get", url, domain=common_domain) + for elem in res: + elem["parent_id"] = parent_id + else: + return [] + return res + + def create(self, data): # pylint: disable=W8106 + if "parent_id" not in data: + raise ValidationError( + _("Attribute id is required to create attribute value on woocommerce.") + ) + res = self._exec( + "post", + "products/attributes/%s/terms" % data["parent_id"], + data=data, + ) + if res: + res.update({"parent_id": data["parent_id"]}) + return res + + def write(self, external_id, data): # pylint: disable=W8106 + return self._exec( + "put", + "products/attributes/%s/terms/%s" % (external_id[0], external_id[1]), + data=data, + ) + + def _get_filters_values(self): + res = super()._get_filters_values() + res.append("slug") + return res diff --git a/connector_woocommerce/models/product_attribute_value/binder.py b/connector_woocommerce/models/product_attribute_value/binder.py new file mode 100644 index 000000000..fe4bb7905 --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/binder.py @@ -0,0 +1,16 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeValueBinder(Component): + _name = "woocommerce.product.attribute.value.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.product.attribute.value" + + external_id = ["parent_id", "id"] + internal_id = ["woocommerce_idattribute", "woocommerce_idattributevalue"] + + external_alt_id = ["parent_name", "name"] diff --git a/connector_woocommerce/models/product_attribute_value/binding.py b/connector_woocommerce/models/product_attribute_value/binding.py new file mode 100644 index 000000000..e982e7373 --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/binding.py @@ -0,0 +1,53 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class WooCommerceProductAttributeValue(models.Model): + _name = "woocommerce.product.attribute.value" + _inherit = "woocommerce.binding" + _inherits = {"product.attribute.value": "odoo_id"} + _description = "WooCommerce Product Attribute Value Binding" + + odoo_id = fields.Many2one( + comodel_name="product.attribute.value", + string="Product attribute value", + required=True, + ondelete="cascade", + ) + woocommerce_idattribute = fields.Integer( + string="ID Attribute", + readonly=True, + ) + woocommerce_idattributevalue = fields.Integer( + string="ID Attribute Value", + readonly=True, + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idattribute,woocommerce_idattributevalue)", + "A binding already exists with the same External (idAttributevalue) ID.", + ), + ] + + @api.model + def _get_base_domain(self): + return [] + + def export_product_attribute_value_since( + self, backend_record=None, since_date=None + ): + domain = self._get_base_domain() + if since_date: + domain += [ + ( + "write_date", + ">", + since_date.strftime("%Y-%m-%dT%H:%M:%S"), + ) + ] + self.export_batch(backend_record, domain=domain) + return True diff --git a/connector_woocommerce/models/product_attribute_value/export_mapper.py b/connector_woocommerce/models/product_attribute_value/export_mapper.py new file mode 100644 index 000000000..d7181b10d --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/export_mapper.py @@ -0,0 +1,44 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import changed_by, mapping +from odoo.addons.connector_extension.components.mapper import required + + +def nullif(field): + def modifier(self, record, to_attr): + value = record[field] + return value and value.strip() or None + + return modifier + + +class WooCommerceProductAttributeValueExportMapper(Component): + _name = "woocommerce.product.attribute.value.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.product.attribute.value" + + @required("name") + @changed_by("name") + @mapping + def name(self, record): + return {"name": record.with_context(lang=self.backend_record.backend_lang).name} + + @required("parent_id") + @changed_by("attribute_id") + @mapping + def parent_id(self, record): + binder = self.binder_for("woocommerce.product.attribute") + values = binder.get_external_dict_ids(record.attribute_id) + return {"parent_id": values["id"] or None} + + @changed_by("attribute_id") + @mapping + def parent_name(self, record): + return { + "parent_name": record.attribute_id.with_context( + lang=self.backend_record.backend_lang + ).name + } diff --git a/connector_woocommerce/models/product_attribute_value/exporter.py b/connector_woocommerce/models/product_attribute_value/exporter.py new file mode 100644 index 000000000..8c7914382 --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/exporter.py @@ -0,0 +1,41 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductAttributeValueBatchDirectExporter(Component): + """Export the WooCommerce Product Attibute Value. + + For every Product Attibute Value in the list, execute inmediately. + """ + + _name = "woocommerce.product.attribute.value.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.product.attribute.value" + + +class WooCommerceProductAttributeValueBatchDelayedExporter(Component): + """Export the WooCommerce Product Attibute Value. + + For every Product Attibute Value in the list, a delayed job is created. + """ + + _name = "woocommerce.product.attribute.value.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.product.attribute.value" + + +class WooCommerceProductAttributeValueExporter(Component): + _name = "woocommerce.product.attribute.value.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.product.attribute.value" + + def _export_dependencies(self, relation): + self._export_dependency( + relation.attribute_id, + "woocommerce.product.attribute", + ) diff --git a/connector_woocommerce/models/product_attribute_value/product_attribute_value.py b/connector_woocommerce/models/product_attribute_value/product_attribute_value.py new file mode 100644 index 000000000..7a9396943 --- /dev/null +++ b/connector_woocommerce/models/product_attribute_value/product_attribute_value.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class ProductAttributeValue(models.Model): + _inherit = "product.attribute.value" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.product.attribute.value", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) diff --git a/connector_woocommerce/models/product_product/__init__.py b/connector_woocommerce/models/product_product/__init__.py new file mode 100644 index 000000000..f4ef6334a --- /dev/null +++ b/connector_woocommerce/models/product_product/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import product diff --git a/connector_woocommerce/models/product_product/adapter.py b/connector_woocommerce/models/product_product/adapter.py new file mode 100644 index 000000000..537fdefe2 --- /dev/null +++ b/connector_woocommerce/models/product_product/adapter.py @@ -0,0 +1,77 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component + + +class WooCommerceProductProductAdapter(Component): + _name = "woocommerce.product.product.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.product.product" + + def read(self, external_id): # pylint: disable=W8106 + external_id = self.binder_for().id2dict(external_id, in_field=False) + url = "products/%s/variations/%s" % ( + external_id["parent_id"], + external_id["id"], + ) + return self._exec("get", url) + + def create(self, data): # pylint: disable=W8106 + self._prepare_data(data) + url_l = ["products"] + parent = data.pop("parent_id") + url_l.append("%s/variations" % parent) + res = self._exec("post", "/".join(url_l), data=data) + res["parent_id"] = parent + return res + + def write(self, external_id, data): # pylint: disable=W8106 + self._prepare_data(data) + return self._exec( + "put", + "products/%s/variations/%s" % (external_id[0], external_id[1]), + data=data, + ) + + def search_read(self, domain=None): + binder = self.binder_for() + domain_dict = self._domain_to_normalized_dict(domain) + external_id_fields = binder.get_id_fields(in_field=False) + _, common_domain = self._extract_domain_clauses(domain, external_id_fields) + external_id = binder.dict2id2dict(domain_dict, in_field=False) + if external_id: + url = "products/%s/variations/%s" % ( + external_id["parent_id"], + external_id["id"], + ) + res = self._exec("get", url, domain=common_domain) + else: + if "id" in domain_dict and "parent_id" in domain_dict: + url = "products/%s/variations/%s" % ( + domain_dict["parent_id"], + domain_dict["id"], + ) + res = self._exec("get", url, domain=common_domain) + elif "sku" in domain_dict: + url = "products" + res = self._exec("get", url, domain=domain) + else: + raise ValidationError(_("Params required")) + return res + + def _get_filters_values(self): + res = super()._get_filters_values() + res.extend(["sku", "parent"]) + return res + + def _format_product_product(self, data): + conv_mapper = { + "/regular_price": lambda x: str(round(x, 10)) or None, + } + self._convert_format(data, conv_mapper) + + def _prepare_data(self, data): + self._format_product_product(data) diff --git a/connector_woocommerce/models/product_product/binder.py b/connector_woocommerce/models/product_product/binder.py new file mode 100644 index 000000000..9f04d7cae --- /dev/null +++ b/connector_woocommerce/models/product_product/binder.py @@ -0,0 +1,17 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductProductBinder(Component): + _name = "woocommerce.product.product.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.product.product" + + external_id = ["parent_id", "id"] + internal_id = ["woocommerce_idparent", "woocommerce_idproduct"] + + external_alt_id = "sku" + internal_alt_id = "default_code" diff --git a/connector_woocommerce/models/product_product/binding.py b/connector_woocommerce/models/product_product/binding.py new file mode 100644 index 000000000..497e29010 --- /dev/null +++ b/connector_woocommerce/models/product_product/binding.py @@ -0,0 +1,67 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.osv import expression + + +class WooCommerceProductProduct(models.Model): + _name = "woocommerce.product.product" + _inherit = "woocommerce.binding" + _inherits = {"product.product": "odoo_id"} + _description = "WooCommerce Product product Binding" + + odoo_id = fields.Many2one( + comodel_name="product.product", + string="Product product", + required=True, + ondelete="cascade", + ) + woocommerce_idproduct = fields.Integer( + string="ID Product", + readonly=True, + ) + woocommerce_idparent = fields.Integer( + string="ID Parent", + readonly=True, + ) + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idproduct)", + "A binding already exists with the same External (idProduct) ID.", + ), + ] + + # TODO: REFACTOR THIS GET_BASE_DOMAIN TO DO IT MORE SIMPLE + @api.model + def _get_base_domain(self): + return [ + ("variant_is_published", "=", True), + ("woocommerce_write_date", "=", False), + ] + + def export_products_since(self, backend_record=None, since_date=None): + domain = self._get_base_domain() + if since_date: + domain = expression.OR( + [ + domain, + [ + ( + "woocommerce_write_date", + ">", + fields.Datetime.to_string(since_date), + ) + ], + ] + ) + self.export_batch(backend_record, domain=domain) + return True + + def resync_export(self): + super().resync_export() + if not self.env.context.get("resync_product_template", False): + self.product_tmpl_id.woocommerce_bind_ids.with_context( + resync_product_product=True + ).resync_export() diff --git a/connector_woocommerce/models/product_product/export_mapper.py b/connector_woocommerce/models/product_product/export_mapper.py new file mode 100644 index 000000000..cb2106860 --- /dev/null +++ b/connector_woocommerce/models/product_product/export_mapper.py @@ -0,0 +1,118 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import changed_by, mapping + + +class WooCommerceProductProductExportMapper(Component): + _name = "woocommerce.product.product.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.product.product" + + @mapping + def price(self, record): + # On WooCommerce regular price is the usually price. + # sales price is the price with discount. + # On odoo we don't have this functionality per product + return { + "regular_price": record.lst_price, + } + + @changed_by("default_code") + @mapping + def sku(self, record): + return {"sku": record.default_code or None} + + @changed_by("is_published") + @mapping + def status(self, record): + return { + "status": "publish" + if record.active and record.variant_is_published + else "private" + } + + @mapping + def stock(self, record): + if record.type in ("consu", "service"): + stock = { + "manage_stock": False, + } + else: + qty = sum( + self.env["stock.quant"] + .search( + [ + ("product_id", "=", record.id), + ( + "location_id", + "child_of", + self.backend_record.stock_location_ids.ids, + ), + ] + ) + .mapped("available_quantity") + ) + stock = { + "manage_stock": True, + # WooCommerce don't accept fractional quantities + "stock_quantity": int(qty), + "stock_status": "instock" if record.qty_available > 0 else "outofstock", + } + return stock + + @mapping + def description(self, record): + return { + "description": record.with_context( + lang=self.backend_record.backend_lang + ).variant_public_description + or None + } + + @mapping + def parent_id(self, record): + binder = self.binder_for("woocommerce.product.template") + values = binder.get_external_dict_ids(record.product_tmpl_id) + return {"parent_id": values["id"]} + + @mapping + def image(self, record): + # WooCommerce only allows one image per variant product + if record.product_attachment_ids and self.collection.wordpress_backend_id: + with self.collection.wordpress_backend_id.work_on( + "wordpress.ir.attachment" + ) as work: + exporter = work.component(self._usage) + binder = exporter.binder_for("wordpress.ir.attachment") + if record.product_attachment_ids: + image = record.product_attachment_ids[0].attachment_id + values = binder.get_external_dict_ids( + image, check_external_id=False + ) + if ( + not values + and self.backend_record.wordpress_backend_id.test_database + ): + return + return { + "image": { + "id": values["id"], + } + } + + @mapping + def attributes(self, record): + binder = self.binder_for("woocommerce.product.attribute") + attr_list = [] + for value in record.product_template_attribute_value_ids: + values = binder.get_external_dict_ids(value.attribute_id) + attr_list.append( + { + "id": values["id"], + "option": value.name, + } + ) + return {"attributes": attr_list} diff --git a/connector_woocommerce/models/product_product/exporter.py b/connector_woocommerce/models/product_product/exporter.py new file mode 100644 index 000000000..20f8f5467 --- /dev/null +++ b/connector_woocommerce/models/product_product/exporter.py @@ -0,0 +1,75 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductProductBatchDirectExporter(Component): + """Export the WooCommerce Product product. + + For every Product product in the list, execute inmediately. + """ + + _name = "woocommerce.product.product.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.product.product" + + +class WooCommerceProductProductBatchDelayedExporter(Component): + """Export the WooCommerce Product Product. + + For every Product product in the list, a delayed job is created. + """ + + _name = "woocommerce.product.product.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.product.product" + + +class WooCommerceProductProductExporter(Component): + _name = "woocommerce.product.product.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.product.product" + + def _export_dependencies(self, relation): + # In the case of a woocommerce simple product (Product template with one variant) + # we need to export the dependencies of the product template because + # it's the product template that will be exported instead of product product + self._export_dependency( + relation.product_tmpl_id, + "woocommerce.product.template", + ) + for attribute_line in relation.attribute_line_ids: + self._export_dependency( + attribute_line.attribute_id, + "woocommerce.product.attribute", + ) + if ( + relation.product_attachment_ids + and len(relation.product_tmpl_id.product_variant_ids) > 1 + ): + if self.collection.wordpress_backend_id: + with self.collection.wordpress_backend_id.work_on( + "wordpress.ir.attachment" + ) as work: + exporter = work.component(self._usage) + exporter._export_dependency( + relation.product_attachment_ids[0].attachment_id, + "wordpress.ir.attachment", + ) + + # This _has_to_skip export the product template instead of the product product + # when the product created in woocommerce is a simple product + def _has_to_skip(self, binding, relation): + res = super()._has_to_skip(binding, relation) + if len(relation.product_tmpl_id.product_variant_ids) <= 1: + self._export_dependency( + relation.product_tmpl_id, + "woocommerce.product.template", + always=True, + ) + res = True + return res diff --git a/connector_woocommerce/models/product_product/product.py b/connector_woocommerce/models/product_product/product.py new file mode 100644 index 000000000..7a70d6f4b --- /dev/null +++ b/connector_woocommerce/models/product_product/product.py @@ -0,0 +1,93 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.product.product", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) + woocommerce_write_date = fields.Datetime( + compute="_compute_woocommerce_write_date", + store=True, + ) + + @api.depends( + "is_published", + "lst_price", + "type", + "default_code", + "image_1920", + "product_tmpl_id", + "default_code", + "qty_available", + "product_template_attribute_value_ids", + "variant_public_description", + ) + def _compute_woocommerce_write_date(self): + for rec in self: + if rec.is_published or rec.woocommerce_write_date: + rec.woocommerce_write_date = fields.Datetime.now() + + variant_public_description = fields.Text( + translate=True, + ) + variant_is_published = fields.Boolean( + default=False, + ) + product_attachment_ids = fields.Many2many( + comodel_name="product.attachment", + compute="_compute_product_attachment_ids", + ) + + def _compute_product_attachment_ids(self): + for rec in self: + attachment = self.env["ir.attachment"].search( + [ + ("res_model", "=", rec._name), + ("res_id", "=", rec.id), + ("res_field", "=", "image_variant_1920"), + ] + ) + if attachment: + rec.product_attachment_ids = [ + ( + 0, + 0, + { + "attachment_id": attachment.id, + "sequence": min( + rec.product_variant_image_ids.mapped("sequence") + ) + - 1 + if rec.product_variant_image_ids + else 1, + }, + ) + ] + for variant_image in rec.product_variant_image_ids: + if variant_image.image_1920: + attachment = self.env["ir.attachment"].search( + [ + ("res_model", "=", variant_image._name), + ("res_id", "in", variant_image.ids), + ("res_field", "=", "image_1920"), + ] + ) + rec.product_attachment_ids = [ + ( + 0, + 0, + { + "attachment_id": attachment.id, + "sequence": variant_image.sequence, + }, + ) + ] + if not rec.product_attachment_ids: + rec.product_attachment_ids = self.env["product.attachment"] diff --git a/connector_woocommerce/models/product_public_category/__init__.py b/connector_woocommerce/models/product_public_category/__init__.py new file mode 100644 index 000000000..055164ff4 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import product_public_category diff --git a/connector_woocommerce/models/product_public_category/adapter.py b/connector_woocommerce/models/product_public_category/adapter.py new file mode 100644 index 000000000..0c1281d10 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/adapter.py @@ -0,0 +1,26 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductPublicCategoryAdapter(Component): + _name = "woocommerce.product.public.category.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.product.public.category" + + def create(self, data): # pylint: disable=W8106 + return self._exec("post", "products/categories", data=data) + + def write(self, external_id, data): # pylint: disable=W8106 + url_l = ["products/categories", str(external_id[0])] + return self._exec("put", "/".join(url_l), data=data) + + def search_read(self, domain=None): + return self._exec("get", "products/categories", domain=domain) + + def _get_filters_values(self): + res = super()._get_filters_values() + res.append("slug") + return res diff --git a/connector_woocommerce/models/product_public_category/binder.py b/connector_woocommerce/models/product_public_category/binder.py new file mode 100644 index 000000000..0112df4c5 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/binder.py @@ -0,0 +1,16 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductPublicCategoryBinder(Component): + _name = "woocommerce.product.public.category.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.product.public.category" + + external_id = "id" + internal_id = "woocommerce_idproduct" + + external_alt_id = "name" diff --git a/connector_woocommerce/models/product_public_category/binding.py b/connector_woocommerce/models/product_public_category/binding.py new file mode 100644 index 000000000..4b557f06f --- /dev/null +++ b/connector_woocommerce/models/product_public_category/binding.py @@ -0,0 +1,45 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class WooCommerceProductPublicCategory(models.Model): + _name = "woocommerce.product.public.category" + _inherit = "woocommerce.binding" + _inherits = {"product.public.category": "odoo_id"} + _description = "WooCommerce Product Public Category Binding" + + odoo_id = fields.Many2one( + comodel_name="product.public.category", + string="Product public category", + required=True, + ondelete="cascade", + ) + woocommerce_idproduct = fields.Integer( + string="ID Product", + readonly=True, + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idproduct)", + "A binding already exists with the same External (idProduct) ID.", + ), + ] + + @api.model + def _get_base_domain(self): + return [] + + def export_product_public_category_since( + self, backend_record=None, since_date=None + ): + domain = self._get_base_domain() + if since_date: + domain += [ + ("write_date", ">", fields.Datetime.to_string(since_date)), + ] + self.export_batch(backend_record, domain=domain) + return True diff --git a/connector_woocommerce/models/product_public_category/export_mapper.py b/connector_woocommerce/models/product_public_category/export_mapper.py new file mode 100644 index 000000000..1f5512254 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/export_mapper.py @@ -0,0 +1,23 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping + + +class WooCommerceProductPublicCategoryExportMapper(Component): + _name = "woocommerce.product.public.category.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.product.public.category" + + direct = [ + ("name", "name"), + ] + + @mapping + def parent_id(self, record): + binder = self.binder_for("woocommerce.product.public.category") + if record.parent_id: + values = binder.get_external_dict_ids(record.parent_id) + return {"parent": values["id"]} diff --git a/connector_woocommerce/models/product_public_category/exporter.py b/connector_woocommerce/models/product_public_category/exporter.py new file mode 100644 index 000000000..0940184a1 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/exporter.py @@ -0,0 +1,41 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductPublicCategoryBatchDirectExporter(Component): + """Export the WooCommerce Product Public Category. + + For every Product Public Category in the list, execute inmediately. + """ + + _name = "woocommerce.product.public.category.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.product.public.category" + + +class WooCommerceProductPublicCategoryBatchDelayedExporter(Component): + """Export the WooCommerce Product Public Category. + + For every Product Public Category in the list, a delayed job is created. + """ + + _name = "woocommerce.product.public.category.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.product.public.category" + + +class WooCommerceProductPublicCategoryExporter(Component): + _name = "woocommerce.product.public.category.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.product.public.category" + + def _export_dependencies(self, relation): + if relation.parent_id: + self._export_dependency( + relation.parent_id, "woocommerce.product.public.category" + ) diff --git a/connector_woocommerce/models/product_public_category/product_public_category.py b/connector_woocommerce/models/product_public_category/product_public_category.py new file mode 100644 index 000000000..48a9f4ed1 --- /dev/null +++ b/connector_woocommerce/models/product_public_category/product_public_category.py @@ -0,0 +1,14 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class ProductPublicCategory(models.Model): + _inherit = "product.public.category" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.product.public.category", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) diff --git a/connector_woocommerce/models/product_template/__init__.py b/connector_woocommerce/models/product_template/__init__.py new file mode 100644 index 000000000..7f01959ff --- /dev/null +++ b/connector_woocommerce/models/product_template/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import product_template diff --git a/connector_woocommerce/models/product_template/adapter.py b/connector_woocommerce/models/product_template/adapter.py new file mode 100644 index 000000000..a46ed7432 --- /dev/null +++ b/connector_woocommerce/models/product_template/adapter.py @@ -0,0 +1,81 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component + + +class WooCommerceProductTemplateAdapter(Component): + _name = "woocommerce.product.template.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.product.template" + + def create(self, data): # pylint: disable=W8106 + self._prepare_data(data) + return self._exec("post", "products", data=data) + + def write(self, external_id, data): # pylint: disable=W8106 + self._prepare_data(data) + url_l = ["products", str(external_id[0])] + res = self._exec("put", "/".join(url_l), data=data) + return res + + def search_read(self, domain=None): + binder = self.binder_for() + domain_dict = self._domain_to_normalized_dict(domain) + id_fields = binder.get_id_fields(in_field=False) + _, common_domain = self._extract_domain_clauses(domain, id_fields) + template_id = binder.dict2id(domain_dict, in_field=False, unwrap=True) + + if template_id: + url = "products/%s" % template_id + res = self._exec("get", url, domain=common_domain) + else: + res = [] + skus = binder.dict2id( + domain_dict, in_field=False, alt_field=True, unwrap=True + ) + if skus and len(skus) > 1: + skus = ",".join([f"{sku}" for sku in skus if sku]) + if skus: + products = self._exec("get", "products", domain=[("sku", "=", skus)]) + if len(products) == 1 and products[0]["type"] == "simple": + return products + parent_ids = set(filter(None, map(lambda x: x["parent_id"], products))) + if len(parent_ids) > 1: + raise ValidationError( + _("All variants must belong to the same parent product") + ) + if parent_ids: + res = [{"id": parent_ids.pop()}] + else: + res = self._exec("get", "products", domain=domain) + return res + + def _get_filters_values(self): + res = super()._get_filters_values() + res.extend(["sku"]) + return res + + def _format_product_template(self, data): + conv_mapper = { + "/regular_price": lambda x: str(round(x, 10)) or None, + } + self._convert_format(data, conv_mapper) + + def _prepare_data(self, data): + self._format_product_template(data) + if data["sku"]: + if data["type"] == "simple": + if len(data["sku"]) > 1: + raise ValidationError( + _("Simple products can only have one variant") + ) + else: + data["sku"] = data["sku"][0] + elif data["type"] == "variable": + data.pop("sku") + else: + raise ValidationError(_("Product type not supported")) diff --git a/connector_woocommerce/models/product_template/binder.py b/connector_woocommerce/models/product_template/binder.py new file mode 100644 index 000000000..a0bec5f4f --- /dev/null +++ b/connector_woocommerce/models/product_template/binder.py @@ -0,0 +1,17 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductTemplateBinder(Component): + _name = "woocommerce.product.template.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.product.template" + + external_id = "id" + internal_id = "woocommerce_idproduct" + + external_alt_id = "sku" + internal_alt_id = "default_code" diff --git a/connector_woocommerce/models/product_template/binding.py b/connector_woocommerce/models/product_template/binding.py new file mode 100644 index 000000000..e0d8eaf23 --- /dev/null +++ b/connector_woocommerce/models/product_template/binding.py @@ -0,0 +1,63 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class WooCommerceProductTemplate(models.Model): + _name = "woocommerce.product.template" + _inherit = "woocommerce.binding" + _inherits = {"product.template": "odoo_id"} + _description = "WooCommerce Product template Binding" + + odoo_id = fields.Many2one( + comodel_name="product.template", + string="Product template", + required=True, + ondelete="cascade", + ) + woocommerce_idproduct = fields.Integer( + string="ID Product", + readonly=True, + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idproduct)", + "A binding already exists with the same External (idProduct) ID.", + ), + ] + + @api.model + def _get_base_domain(self): + return [("website_published", "=", True)] + + def export_product_tmpl_since(self, backend_record=None, since_date=None): + domain = self._get_base_domain() + if since_date: + domain += [ + ("woocommerce_write_date", ">", fields.Datetime.to_string(since_date)), + ] + self.export_batch(backend_record, domain=domain) + return True + + # def export_up_sell_products_since(self, backend_record=None, since_date=None): + # domain = self._get_base_domain() + # if since_date: + # domain += [ + # ( + # "woocommerce_upsell_write_date", + # ">", + # fields.Datetime.to_string(since_date), + # ), + # ] + # self.export_up_sell_products_batch(backend_record, domain=domain) + # return True + + def resync_export(self): + super().resync_export() + if not self.env.context.get("resync_product_product", False): + self.product_variant_ids.woocommerce_bind_ids.with_context( + resync_product_template=True + ).resync_export() diff --git a/connector_woocommerce/models/product_template/export_mapper.py b/connector_woocommerce/models/product_template/export_mapper.py new file mode 100644 index 000000000..385e6fb4c --- /dev/null +++ b/connector_woocommerce/models/product_template/export_mapper.py @@ -0,0 +1,202 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import changed_by, mapping + + +class WooCommerceProductTemplateExportMapper(Component): + _name = "woocommerce.product.template.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.product.template" + + @mapping + def name(self, record): + return {"name": record.name} + + @changed_by("default_code") + @mapping + def sku(self, record): + default_codes = record.product_variant_ids.filtered("default_code").mapped( + "default_code" + ) + return {"sku": default_codes or None} + + @mapping + def status(self, record): + return { + "status": "publish" if record.active and record.is_published else "private" + } + + @mapping + def stock(self, record): + if record.type in ("consu", "service"): + manage_stock = False + else: + manage_stock = True + if len(record.product_variant_ids) <= 1: + qty = sum( + self.env["stock.quant"] + .search( + [ + ("product_id", "=", record.product_variant_id.id), + ( + "location_id", + "child_of", + self.backend_record.stock_location_ids.ids, + ), + ] + ) + .mapped("available_quantity") + ) + return { + "manage_stock": manage_stock, + # TODO: modificar la quantity per agafar les dels magatzems definits al backend + "stock_quantity": int(qty), + "stock_status": "instock" + if record.product_variant_id.qty_available > 0 + or record.type in ("consu", "service") + else "outofstock", + } + else: + return { + "manage_stock": False, + } + + @mapping + def price(self, record): + if len(record.product_variant_ids) <= 1: + # On WooCommerce regular price is the usually price. + # sales price is the price with discount. + # On odoo we don't have this functionality per product + return { + "regular_price": record.list_price, + } + + @mapping + def description(self, record): + description = False + if record.public_description: + description = record.with_context( + lang=self.backend_record.backend_lang + ).public_description + elif ( + len(record.product_variant_ids) == 1 + and record.product_variant_id.variant_public_description + ): + description = record.product_variant_id.with_context( + lang=self.backend_record.backend_lang + ).variant_public_description + return {"description": description if description else ""} + + @mapping + def product_type(self, record): + product_type = "simple" + if len(record.product_variant_ids) > 1: + product_type = "variable" + return {"type": product_type} + + @mapping + def categories(self, record): + categories = [] + binder = self.binder_for("woocommerce.product.public.category") + for category in record.public_categ_ids: + values = binder.get_external_dict_ids(category) + categories.append({"id": values["id"]}) + if categories: + return {"categories": categories} + + @mapping + def attributes(self, record): + binder = self.binder_for("woocommerce.product.attribute") + attr_list = [] + for line in record.attribute_line_ids: + values = binder.get_external_dict_ids(line.attribute_id) + attr_list.append( + { + "id": values["id"], + "options": line.value_ids.with_context( + lang=self.backend_record.backend_lang + ).mapped("name"), + "visible": "true", + "variation": "true", + } + ) + if attr_list: + return {"attributes": attr_list} + + @mapping + def tax_class(self, record): + if record.taxes_id: + if len(record.taxes_id) > 1: + raise ValidationError(_("Only one tax is allowed per product")) + tax_class = self.backend_record.tax_class_ids.filtered( + lambda x: record["taxes_id"] == x.account_tax + ) + if not tax_class: + raise ValidationError( + _("Tax class is not defined on backend for tax %s") + % record.mapped("taxes_id").name + ) + return {"tax_class": tax_class.woocommerce_tax_class} + + # @mapping + # def upsell_ids(self, record): + # binder = self.binder_for("woocommerce.product.template") + # alternate_list = [] + # if record.alternative_product_ids: + # for product in record.alternative_product_ids: + # values = binder.get_external_dict_ids(product) + # alternate_list.append(values["id"]) + # if alternate_list: + # # return {"cross_sell_ids": alternate_list} + # return {"upsell_ids": alternate_list} + # + # @mapping + # def cross_sell_ids(self, record): + # binder = self.binder_for("woocommerce.product.product") + # accessory_list = [] + # if record.accessory_product_ids: + # for product in record.accessory_product_ids: + # values = binder.get_external_dict_ids(product) + # accessory_list.append(values["id"]) + # if accessory_list: + # return {"cross_sell_ids": accessory_list} + + @mapping + def images(self, record): + if self.collection.wordpress_backend_id: + with self.collection.wordpress_backend_id.work_on( + "wordpress.ir.attachment" + ) as work: + exporter = work.component(self._usage) + binder = exporter.binder_for("wordpress.ir.attachment") + img_list = [] + if record.product_attachment_ids: + for image in record.product_attachment_ids.mapped("attachment_id"): + external_id = binder.get_external_dict_ids( + image, check_external_id=False + ) + if external_id: + img_list.append( + { + "id": external_id["id"], + } + ) + else: + if ( + self.backend_record.wordpress_backend_id + and not self.backend_record.wordpress_backend_id.test_database + ): + assert external_id, ( + "Unexpected error on %s:" + "The backend id cannot be obtained." + "At this stage, the backend record should " + "have been already linked via " + "._export_dependencies. " % record._name + ) + if img_list: + return {"images": img_list} diff --git a/connector_woocommerce/models/product_template/exporter.py b/connector_woocommerce/models/product_template/exporter.py new file mode 100644 index 000000000..65ed74898 --- /dev/null +++ b/connector_woocommerce/models/product_template/exporter.py @@ -0,0 +1,111 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceProductTemplateBatchDirectExporter(Component): + """Export the WooCommerce Product template. + + For every Product template in the list, execute inmediately. + """ + + _name = "woocommerce.product.template.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.product.template" + + +class WooCommerceProductTemplateBatchDelayedExporter(Component): + """Export the WooCommerce Product Template. + + For every Product template in the list, a delayed job is created. + """ + + _name = "woocommerce.product.template.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.product.template" + + +class WooCommerceProductTemplateExporter(Component): + _name = "woocommerce.product.template.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.product.template" + + def _export_dependencies(self, relation): + for category in relation.public_categ_ids: + self._export_dependency(category, "woocommerce.product.public.category") + for line in relation.attribute_line_ids: + self._export_dependency( + line.attribute_id, + "woocommerce.product.attribute", + ) + for value in line.value_ids: + self._export_dependency( + value, + "woocommerce.product.attribute.value", + ) + # for alternative_product in relation.alternative_product_ids: + # rel_id = relation.id + # ap = self.model._context.get("alternative_product") + # alternative_product_list=[] + # if ap: + # if rel_id not in ap: + # alternative_product_list= ap.append(rel_id) + # else: + # self.model.with_context(export_wo_ap=True) + # self._export_dependency( + # alternative_product, + # "woocommerce.product.template", + # ) + # else: + # alternative_product_list = [rel_id] + # self.model.with_context(alternative_product=alternative_product_list) + # # context = self.model._context.copy() + # # context["aaa"] = [1,2,3] + # # self.model._context = context + # self._export_dependency( + # alternative_product, + # "woocommerce.product.template", + # ) + + if self.collection.wordpress_backend_id: + with self.collection.wordpress_backend_id.work_on( + "wordpress.ir.attachment" + ) as work: + exporter = work.component(self._usage) + for attachment in relation.product_attachment_ids: + exporter._export_dependency( + attachment.attachment_id, + "wordpress.ir.attachment", + ) + # + # if self._context.get("alternative_product"): + # if relation.id not in self._context["alternative_product"]: + # new_context = dict(self._context) + # new_context["alternative_product"].append(relation.id) + # self.with_context(new_context).export_record(relation) + # + # for alternative_product in relation.alternative_product_ids: + # if self.model._context.get("alternative_products"): + # if relation.id in self._context["alternative_products"]: + # return + # if self.model._context.get("alternative_products"): + # if alternative_product.id not in self.with_context( + # "alternative_product" + # ): + # new_context = dict(self._context) + # new_context["alternative_product"].append(alternative_product.id) + # self.with_context( + # alternative_products=new_context + # )._export_dependency( + # alternative_product, "woocommerce.product.template" + # ) + # # self.with_context(export_ap=True).\ + # # _export_dependency(alternative_product, "woocommerce.product.template") + # else: + # self.with_context(export_wo_ap=True)._export_dependency( + # alternative_product, "woocommerce.product.template" + # ) diff --git a/connector_woocommerce/models/product_template/product_template.py b/connector_woocommerce/models/product_template/product_template.py new file mode 100644 index 000000000..dd7e80921 --- /dev/null +++ b/connector_woocommerce/models/product_template/product_template.py @@ -0,0 +1,119 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.product.template", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) + woocommerce_write_date = fields.Datetime( + compute="_compute_woocommerce_write_date", + store=True, + ) + # woocommerce_upsell_write_date = fields.Datetime( + # compute="_compute_woocommerce_write_date", + # store=True, + # ) + + @api.depends( + "is_published", + "name", + "lst_price", + "active", + "product_variant_id.qty_available", + "image_1920", + "default_code", + "qty_available", + "description", + "public_categ_ids", + "attribute_line_ids", + "public_description", + ) + def _compute_woocommerce_write_date(self): + for rec in self: + if rec.is_published or rec.woocommerce_write_date: + rec.woocommerce_write_date = fields.Datetime.now() + + public_description = fields.Text( + translate=True, + ) + is_published = fields.Boolean( + related="template_is_published", + store=True, + readonly=False, + ) + template_is_published = fields.Boolean( + compute="_compute_template_is_published", + inverse="_inverse_template_is_published", + store=True, + ) + + @api.depends("product_variant_ids.variant_is_published") + def _compute_template_is_published(self): + for rec in self: + published_variants = rec._origin.product_variant_ids.filtered( + lambda x: x.variant_is_published + ) + rec.template_is_published = bool(published_variants) + + def _inverse_template_is_published(self): + for rec in self: + rec.product_variant_ids.variant_is_published = rec.template_is_published + + product_attachment_ids = fields.Many2many( + comodel_name="product.attachment", + compute="_compute_product_attachment_ids", + ) + + def _compute_product_attachment_ids(self): + for rec in self: + attachment = self.env["ir.attachment"].search( + [ + ("res_model", "=", rec._name), + ("res_id", "=", rec.id), + ("res_field", "=", "image_1920"), + ] + ) + if attachment: + rec.product_attachment_ids = [ + ( + 0, + 0, + { + "attachment_id": attachment.id, + "sequence": min( + rec.product_template_image_ids.mapped("sequence") + ) + - 1 + if rec.product_template_image_ids + else 1, + }, + ) + ] + for template_image in rec.product_template_image_ids: + if template_image.image_1920: + attachment = self.env["ir.attachment"].search( + [ + ("res_model", "=", template_image._name), + ("res_id", "=", template_image.id), + ("res_field", "=", "image_1920"), + ] + ) + rec.product_attachment_ids = [ + ( + 0, + 0, + { + "attachment_id": attachment.id, + "sequence": template_image.sequence, + }, + ) + ] + if not rec.product_attachment_ids: + rec.product_attachment_ids = self.env["product.attachment"] diff --git a/connector_woocommerce/models/res_partner/__init__.py b/connector_woocommerce/models/res_partner/__init__.py new file mode 100644 index 000000000..fade27bb6 --- /dev/null +++ b/connector_woocommerce/models/res_partner/__init__.py @@ -0,0 +1,6 @@ +from . import adapter +from . import binder +from . import binding +from . import import_mapper +from . import importer +from . import res_partner diff --git a/connector_woocommerce/models/res_partner/adapter.py b/connector_woocommerce/models/res_partner/adapter.py new file mode 100644 index 000000000..95edbc93b --- /dev/null +++ b/connector_woocommerce/models/res_partner/adapter.py @@ -0,0 +1,11 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceResPartnerAdapter(Component): + _name = "woocommerce.res.partner.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.res.partner" diff --git a/connector_woocommerce/models/res_partner/binder.py b/connector_woocommerce/models/res_partner/binder.py new file mode 100644 index 000000000..e7ebb0a3c --- /dev/null +++ b/connector_woocommerce/models/res_partner/binder.py @@ -0,0 +1,19 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceResPartnerBinder(Component): + _name = "woocommerce.res.partner.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.res.partner" + + external_id = ["type", "hash"] + internal_id = ["woocommerce_address_type", "woocommerce_address_hash"] + external_alt_id = ["email", "hash", "type"] + internal_alt_id = [ + "address_hash", + "type", + ] diff --git a/connector_woocommerce/models/res_partner/binding.py b/connector_woocommerce/models/res_partner/binding.py new file mode 100644 index 000000000..507b0f286 --- /dev/null +++ b/connector_woocommerce/models/res_partner/binding.py @@ -0,0 +1,38 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class WooCommerceResPartner(models.Model): + _name = "woocommerce.res.partner" + _inherit = "woocommerce.binding" + _inherits = {"res.partner": "odoo_id"} + _description = "WooCommerce Res Partner Binding" + + odoo_id = fields.Many2one( + comodel_name="res.partner", + string="Res Partner", + required=True, + ondelete="cascade", + ) + woocommerce_idrespartner = fields.Integer( + string="ID Res Partner", + readonly=True, + ) + woocommerce_address_type = fields.Char( + string="WooCommerce Type", + readonly=True, + ) + woocommerce_address_hash = fields.Char( + string="Address Hash", + readonly=True, + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idrespartner)", + "A binding already exists with the same External (idResPartner) ID.", + ), + ] diff --git a/connector_woocommerce/models/res_partner/import_mapper.py b/connector_woocommerce/models/res_partner/import_mapper.py new file mode 100644 index 000000000..2f1315945 --- /dev/null +++ b/connector_woocommerce/models/res_partner/import_mapper.py @@ -0,0 +1,73 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping + + +class WooCommerceResPartnerImportMapper(Component): + _name = "woocommerce.res.partner.import.mapper" + _inherit = "woocommerce.import.mapper" + + _apply_on = "woocommerce.res.partner" + + @mapping + def name(self, record): + return {"name": record.get("first_name") + " " + record.get("last_name")} + + @mapping + def parent_id(self, record): + return {"parent_id": record.get("parent") or None} + + @mapping + def street(self, record): + return {"street": record.get("address_1")} + + @mapping + def street2(self, record): + return {"street2": record.get("address_2")} + + @mapping + def city(self, record): + return {"city": record.get("city")} + + @mapping + def type(self, record): + address_type = record.get("type") + if address_type == "billing": + return {"type": "invoice"} + elif address_type == "shipping": + return {"type": "delivery"} + return {"type": record.get("type")} + + @mapping + def hash(self, record): + return {"address_hash": record["hash"]} + + @mapping + def state_id(self, record): + state = self.env["res.country.state"].search( + [ + ("code", "=", record["state"]), + ("country_id.code", "=", record["country"]), + ] + ) + if state: + return {"state_id": state.id} + + @mapping + def zip(self, record): + return {"zip": record.get("postcode")} + + @mapping + def country_id(self, record): + country = self.env["res.country"].search([("code", "=", record.get("country"))]) + if country: + return {"country_id": country.id or None} + + @mapping + def email(self, record): + return {"email": record.get("email")} + + @mapping + def mobile(self, record): + return {"mobile": record.get("phone")} diff --git a/connector_woocommerce/models/res_partner/importer.py b/connector_woocommerce/models/res_partner/importer.py new file mode 100644 index 000000000..a09df852c --- /dev/null +++ b/connector_woocommerce/models/res_partner/importer.py @@ -0,0 +1,35 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceResPartnerBatchDirectImporter(Component): + """Import the WooCommerce Res Partner. + + For every Res Partner in the list, execute inmediately. + """ + + _name = "woocommerce.res.partner.batch.direct.importer" + _inherit = "generic.batch.direct.importer" + + _apply_on = "woocommerce.res.partner" + + +class WooCommerceResPartnerBatchDelayedImporter(Component): + """Import the WooCommerce Res Partner. + + For every Res Partner in the list, a delayed job is created. + """ + + _name = "woocommerce.res.partner.batch.delayed.importer" + _inherit = "generic.batch.delayed.importer" + + _apply_on = "woocommerce.res.partner" + + +class WooCommerceResPartnerImporter(Component): + _name = "woocommerce.res.partner.record.direct.importer" + _inherit = "woocommerce.record.direct.importer" + + _apply_on = "woocommerce.res.partner" diff --git a/connector_woocommerce/models/res_partner/res_partner.py b/connector_woocommerce/models/res_partner/res_partner.py new file mode 100644 index 000000000..28dd0824e --- /dev/null +++ b/connector_woocommerce/models/res_partner/res_partner.py @@ -0,0 +1,49 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + +from ....connector_extension.common.tools import list2hash + + +class Partner(models.Model): + _inherit = "res.partner" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.res.partner", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) + + address_hash = fields.Char( + compute="_compute_address_hash", store=True, readonly=True + ) + + @api.model + def _get_hash_fields(self): + return ["name", "street", "street2", "city", "zip", "email", "mobile"] + + def _set_values_hash(self): + for rec in self: + values = [rec[x] or None for x in self._get_hash_fields()] + values.append(rec.parent_id.name or None) + values.append(rec.state_id.code or None) + values.append(rec.country_id.code or None) + return values + + @api.depends( + "name", + "parent_id", + "street", + "street2", + "city", + "state_id", + "zip", + "country_id", + "email", + "mobile", + ) + def _compute_address_hash(self): + for rec in self: + values = rec._set_values_hash() + rec.address_hash = list2hash(values) diff --git a/connector_woocommerce/models/sale_order/__init__.py b/connector_woocommerce/models/sale_order/__init__.py new file mode 100644 index 000000000..20355b946 --- /dev/null +++ b/connector_woocommerce/models/sale_order/__init__.py @@ -0,0 +1,9 @@ +from . import adapter +from . import binder +from . import binding +from . import export_mapper +from . import exporter +from . import import_mapper +from . import importer +from . import listener +from . import sale_order diff --git a/connector_woocommerce/models/sale_order/adapter.py b/connector_woocommerce/models/sale_order/adapter.py new file mode 100644 index 000000000..ce4ac1660 --- /dev/null +++ b/connector_woocommerce/models/sale_order/adapter.py @@ -0,0 +1,175 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component + +from ....connector_extension.common.tools import list2hash + + +class WooCommerceSaleOrderAdapter(Component): + _name = "woocommerce.sale.order.adapter" + _inherit = "connector.woocommerce.adapter" + + _apply_on = "woocommerce.sale.order" + + def get_total_items(self, resource=None, domain=None): + return super().get_total_items("orders", domain=domain) + + def read(self, external_id): # pylint: disable=W8106 + external_id = self.binder_for().id2dict(external_id, in_field=False) + url = "orders/%s" % (external_id["id"]) + res = self._exec("get", url, limit=1) + self._reorg_order_data(res) + if len(res) > 1: + raise ValidationError( + _("More than one order found with the same id: %s") + % (external_id["id"]) + ) + return res[0] + + def search_read(self, domain=None, offset=0, limit=None): + self._convert_format_domain(domain) + if limit: + res = self._exec("get", "orders", domain=domain, offset=offset, limit=limit) + else: + res = self._exec("get", "orders", domain=domain, offset=offset) + self._reorg_order_data(res) + return res, len(res) + + def write(self, external_id, data): # pylint: disable=W8106 + url_l = ["orders", str(external_id[0])] + return self._exec("put", "/".join(url_l), data=data) + + def _get_partner_parent(self, dir_type, value): + # TODO: REVIEW: slug for company name? + domain = [ + ("name", "=", value[dir_type]["company"]), + ("company_type", "=", "company"), + ] + if value[dir_type].get("nif"): + domain.append(("vat", "=", value[dir_type]["nif"])) + parent = self.env["res.partner"].search(domain) + if not parent: + parent = self.env["res.partner"].create( + { + "name": value[dir_type]["company"], + "company_type": "company", + "vat": value[dir_type].get("nif"), + } + ) + value[dir_type]["parent"] = parent.id + elif len(parent) > 1: + raise ValidationError( + _("There are more than one partner with the same name") + ) + else: + value[dir_type]["parent"] = parent.id + + def _get_hash_fields(self): + return [ + "name", + "address_1", + "address_2", + "city", + "postcode", + "email", + "phone", + "company", + "state", + "country", + ] + + def _get_billing(self, value, hash_fields): + if value.get("billing"): + value["billing"]["type"] = "billing" + if value["billing"].get("company"): + self._get_partner_parent("billing", value) + value["billing"]["name"] = ( + value["billing"]["first_name"] + " " + value["billing"]["last_name"] + ) + value["billing"]["hash"] = list2hash( + value["billing"].get(x) for x in hash_fields + ) + + def _get_shipping(self, value, hash_fields): + if value.get("shipping"): + if value["shipping"].get("first_name") or value["shipping"].get( + "last_name" + ): + if value["shipping"].get("company"): + self._get_partner_parent("shipping", value) + value["shipping"]["type"] = "shipping" + if value["shipping"].get("first_name"): + value["shipping"]["name"] = value["shipping"]["first_name"] + if value["shipping"].get("last_name"): + value["shipping"]["name"] += ( + " " + value["shipping"]["last_name"] + ) + else: + value["shipping"]["name"] = value["shipping"]["last_name"] + value["shipping"]["hash"] = list2hash( + value["shipping"].get(x) for x in hash_fields + ) + else: + value["shipping"] = None + else: + value["shipping"] = None + + def _reorg_order_data(self, values): + # reorganize data + for value in values: + hash_fields = self._get_hash_fields() + self._get_billing(value, hash_fields) + self._get_shipping(value, hash_fields) + if not value.get("products"): + value["products"] = [] + for item in value["line_items"]: + item["order_id"] = value["id"] + item["is_shipping"] = False + if not float(item["subtotal"]): + item["discount"] = 0 + else: + item["discount"] = ( + 1 - float(item["total"]) / float(item["subtotal"]) + ) * 100 + product = { + "id": item["variation_id"] + if item.get("variation_id") + else item["product_id"], + "sku": item["sku"], + "name": item["name"], + "type": "variation" if item.get("variation_id") else "simple", + } + if item.get("variation_id"): + product["parent_id"] = item["product_id"] + value["products"].append(product) + for shipping in value["shipping_lines"]: + value["line_items"].append( + { + "order_id": value["id"], + "is_shipping": True, + "taxes": shipping["taxes"], + "total": shipping["total"], + "subtotal": shipping["total"], + "quantity": 1, + "id": shipping["id"], + "discount": 0, + "total_tax": shipping["total_tax"], + } + ) + + def _get_filters_values(self): + res = super()._get_filters_values() + res += [ + "status", + "after", + "date_created_gmt", + "modified_after", + "modified_before", + ] + return res + + def _prepare_domain(self, domain): + self._convert_format_domain(domain) diff --git a/connector_woocommerce/models/sale_order/binder.py b/connector_woocommerce/models/sale_order/binder.py new file mode 100644 index 000000000..da9c24a0b --- /dev/null +++ b/connector_woocommerce/models/sale_order/binder.py @@ -0,0 +1,16 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceSaleOrderBinder(Component): + _name = "woocommerce.sale.order.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.sale.order" + + external_id = "id" + internal_id = "woocommerce_idsaleorder" + + internal_alt_id = "client_order_ref" diff --git a/connector_woocommerce/models/sale_order/binding.py b/connector_woocommerce/models/sale_order/binding.py new file mode 100644 index 000000000..6f8d87b87 --- /dev/null +++ b/connector_woocommerce/models/sale_order/binding.py @@ -0,0 +1,71 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class WooCommerceSaleOrder(models.Model): + _name = "woocommerce.sale.order" + _inherit = "woocommerce.binding" + _inherits = {"sale.order": "odoo_id"} + _description = "WooCommerce Sale Order Binding" + + odoo_id = fields.Many2one( + comodel_name="sale.order", + string="Sale Order", + required=True, + ondelete="cascade", + ) + woocommerce_idsaleorder = fields.Integer( + string="ID Sale Order", + readonly=True, + ) + woocommerce_status = fields.Char( + readonly=True, + ) + woocommerce_order_line_ids = fields.One2many( + string="WooCommerce Order Line ids", + help="Order Lines in WooCommerce sale orders", + comodel_name="woocommerce.sale.order.line", + inverse_name="woocommerce_order_id", + ) + + _sql_constraints = [ + ( + "external_uniq", + "unique(backend_id, woocommerce_idsaleorder)", + "A binding already exists with the same External (idSaleOrder) ID.", + ), + ] + + @api.model + def _get_base_domain(self): + return [] + + def import_sale_orders_since(self, backend_record=None, since_date=None): + domain = self._get_base_domain() + # TODO: El extract_domain_clauses don't accept 'in operator, + # to use get_total_items in status on-hold and processing we have to do two imports. + # We have to find a better way to join this domains. + domain += [("status", "=", "on-hold,processing")] + # domain += [("status", "=", "on-hold")] + # domain += [("status", "in", ["on-hold","processing"])] + + if since_date: + domain += [("after", "=", since_date)] + self.import_batch(backend_record, domain=domain) + return True + + def export_sale_orders_since(self, backend_record=None, since_date=None): + domain = self._get_base_domain() + domain += [("woocommerce_bind_ids", "!=", False)] + if since_date: + domain += [ + ( + "woocommerce_status_write_date", + ">", + since_date.strftime("%Y-%m-%dT%H:%M:%S"), + ) + ] + self.export_batch(backend_record, domain=domain) + return True diff --git a/connector_woocommerce/models/sale_order/export_mapper.py b/connector_woocommerce/models/sale_order/export_mapper.py new file mode 100644 index 000000000..3cd33c54d --- /dev/null +++ b/connector_woocommerce/models/sale_order/export_mapper.py @@ -0,0 +1,22 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping + + +class WooCommerceSaleOrderExportMapper(Component): + _name = "woocommerce.sale.order.export.mapper" + _inherit = "woocommerce.export.mapper" + + _apply_on = "woocommerce.sale.order" + + @mapping + def status(self, record): + if record.woocommerce_order_state == "processing": + status = "on-hold" if record.state == "draft" else "processing" + elif record.woocommerce_order_state == "done": + status = "completed" + else: + status = "cancelled" + return {"status": status} diff --git a/connector_woocommerce/models/sale_order/exporter.py b/connector_woocommerce/models/sale_order/exporter.py new file mode 100644 index 000000000..5a8ebdab2 --- /dev/null +++ b/connector_woocommerce/models/sale_order/exporter.py @@ -0,0 +1,35 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceSaleOrdertBatchDirectExporter(Component): + """Export the WooCommerce Sale Order. + + For every Sale Order in the list, execute inmediately. + """ + + _name = "woocommerce.sale.order.batch.direct.exporter" + _inherit = "generic.batch.direct.exporter" + + _apply_on = "woocommerce.sale.order" + + +class WooCommerceSaleOrderBatchDelayedExporter(Component): + """Export the WooCommerce Sale Order. + + For every Sale Order in the list, a delayed job is created. + """ + + _name = "woocommerce.sale.order.batch.delayed.exporter" + _inherit = "generic.batch.delayed.exporter" + + _apply_on = "woocommerce.sale.order" + + +class WooCommerceSaleOrderExporter(Component): + _name = "woocommerce.sale.order.record.direct.exporter" + _inherit = "woocommerce.record.direct.exporter" + + _apply_on = "woocommerce.sale.order" diff --git a/connector_woocommerce/models/sale_order/import_mapper.py b/connector_woocommerce/models/sale_order/import_mapper.py new file mode 100644 index 000000000..5d35b4b0b --- /dev/null +++ b/connector_woocommerce/models/sale_order/import_mapper.py @@ -0,0 +1,119 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create + + +class SaleOrderImportMapChild(Component): + _name = "woocommerce.sale.order.map.child.import" + _inherit = "woocommerce.map.child.import" + + _apply_on = "woocommerce.sale.order.line" + + def get_item_values(self, map_record, to_attr, options): + binder = self.binder_for("woocommerce.sale.order.line") + external_id = binder.dict2id(map_record.source, in_field=False) + woocommerce_order_line = binder.to_internal(external_id, unwrap=False) + if woocommerce_order_line: + map_record.update(id=woocommerce_order_line.id) + return map_record.values(**options) + + def format_items(self, items_values): + ops = [] + for values in items_values: + _id = values.pop("id", None) + if _id: + ops.append((1, _id, values)) + else: + ops.append((0, False, values)) + return ops + + +class WooCommerceSaleOrderImportMapper(Component): + _name = "woocommerce.sale.order.import.mapper" + _inherit = "woocommerce.import.mapper" + + _apply_on = "woocommerce.sale.order" + children = [ + ("line_items", "woocommerce_order_line_ids", "woocommerce.sale.order.line") + ] + + @mapping + def billing(self, record): + if record["billing"]: + binder = self.binder_for("woocommerce.res.partner") + external_id = binder.dict2id(record["billing"], in_field=False) + partner = binder.to_internal(external_id, unwrap=True) + assert partner, ( + "partner_id %s should have been imported in " + "SaleOrderImporter._import_dependencies" % external_id + ) + if not partner.active: + raise ValidationError( + _("The partner %s, with id:%s is archived, please, enable it") + % (partner.name, partner.id) + ) + partner_return = {"partner_invoice_id": partner.id} + if partner.parent_id: + partner_return["partner_id"] = partner.parent_id.id + else: + partner_return["partner_id"] = partner.id + return partner_return + + @mapping + def shipping(self, record): + if record["shipping"]: + binder = self.binder_for("woocommerce.res.partner") + external_id = binder.dict2id(record["shipping"], in_field=False) + partner = binder.to_internal(external_id, unwrap=True) + assert partner, ( + "partner_shipping_id %s should have been imported in " + "SaleOrderImporter._import_dependencies" % external_id + ) + return {"partner_shipping_id": partner.id} + + @mapping + def payment_method(self, record): + payment_mode = self.backend_record.payment_mode_ids.filtered( + lambda x: record["payment_method"] == x.woocommerce_payment_mode + ) + if not payment_mode: + raise ValidationError( + _("Payment method '%s' is not defined on backend") + % record.get("payment_method") + ) + return {"payment_mode_id": payment_mode.payment_mode_id.id} + + @mapping + def currency(self, record): + currency = self.env["res.currency"].search( + [("name", "=", record.get("currency"))] + ) + if not currency: + raise ValidationError( + _("Currency '%s' is not defined") % record.get("currency") + ) + return {"currency_id": currency.id} + + @mapping + def woocommerce_order_id(self, record): + client_order_ref = ( + self.backend_record.client_order_ref_prefix + "-" + str(record["id"]) + ) + return {"client_order_ref": client_order_ref} + + @mapping + def note(self, record): + return {"note": record["customer_note"]} + + @mapping + def status(self, record): + return {"woocommerce_status": record["status"]} + + @only_create + @mapping + def is_woocommerce(self, record): + return {"is_woocommerce": True} diff --git a/connector_woocommerce/models/sale_order/importer.py b/connector_woocommerce/models/sale_order/importer.py new file mode 100644 index 000000000..320139a12 --- /dev/null +++ b/connector_woocommerce/models/sale_order/importer.py @@ -0,0 +1,129 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component + + +class WooCommerceSaleOrderBatchDirectImporter(Component): + """Import the WooCommerce Sale Order. + + For every Sale Order in the list, execute inmediately. + """ + + _name = "woocommerce.sale.order.batch.direct.importer" + _inherit = "generic.batch.direct.importer" + + _apply_on = "woocommerce.sale.order" + + +class WooCommerceSaleOrderBatchDelayedImporter(Component): + """Import the WooCommerce Sale Order. + + For every Sale Order in the list, a delayed job is created. + """ + + _name = "woocommerce.sale.order.batch.delayed.importer" + _inherit = "generic.batch.delayed.importer" + + _apply_on = "woocommerce.sale.order" + + +class WooCommerceSaleOrderImporter(Component): + _name = "woocommerce.sale.order.record.direct.importer" + _inherit = "woocommerce.record.direct.importer" + + _apply_on = "woocommerce.sale.order" + + def _import_dependencies(self, external_data, sync_date): + # Customer + binder = self.binder_for("woocommerce.res.partner") + billing = external_data.get("billing") + if billing: + self._import_dependency( + binder.dict2id(billing, in_field=False), + "woocommerce.res.partner", + sync_date, + external_data=billing, + ) + shipping = external_data.get("shipping") + if shipping: + self._import_dependency( + binder.dict2id(shipping, in_field=False), + "woocommerce.res.partner", + sync_date, + external_data=shipping, + ) + # Products + # We have done this so that a product can be linked with external id or sku + # without having to call the export dependency and thus avoid the write. + # Could we do it another way? + products = external_data["products"] + for product in products: + if product["type"] == "simple": + model = "product.template" + if product["id"] == 0: + raise ValidationError( + _( + "The product '%s' in the order has been deleted on WooCommerce. " + "This order cannot be imported." + ) + % product["name"] + ) + else: + model = "product.product" + if product["id"] == 0 and product["parent_id"] == 0: + raise ValidationError( + _( + "The product '%s' in the order has been deleted on WooCommerce. " + "This order cannot be imported." + ) + % product["name"] + ) + + binder = self.binder_for("woocommerce." + model) + external_id = binder.dict2id(product, in_field=False) + binding = binder.to_internal(external_id) + if not binding and product.get("sku"): + relation = self.env[model].search( + [("default_code", "=", product["sku"])] + ) + if not relation: + raise ValidationError(_("Product not found on Odoo")) + if len(relation) > 1: + raise ValidationError( + _("More than one product found with sku %s") % product["sku"] + ) + binding = binder.bind_export(product, relation) + if not binding: + raise ValidationError(_("Product not found on Odoo")) + + def _after_import(self, binding): + sale_order = self.binder_for().unwrap_binding(binding) + if sale_order.state == "draft" and binding.woocommerce_status == "processing": + sale_order.action_confirm() + + +class WooCommerceSaleOrderChunkDirectImporter(Component): + """Import the Woocommerce Orders. + + For every order in the list, import it directly. + """ + + _name = "woocommerce.sale.order.chunk.direct.importer" + _inherit = "generic.chunk.direct.importer" + + _apply_on = "woocommerce.sale.order" + + +class WooCommerceSaleOrderChunkDelayedImporter(Component): + """Import the Woocommerce Orders. + + For every order in the list, a delayed job is created. + """ + + _name = "woocommerce.sale.order.chunk.delayed.importer" + _inherit = "generic.chunk.delayed.importer" + + _apply_on = "woocommerce.sale.order" diff --git a/connector_woocommerce/models/sale_order/listener.py b/connector_woocommerce/models/sale_order/listener.py new file mode 100644 index 000000000..54e54594f --- /dev/null +++ b/connector_woocommerce/models/sale_order/listener.py @@ -0,0 +1,35 @@ +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class WooCommerceSaleOrderListener(Component): + _name = "sale.order.listener" + _inherit = "base.event.listener" + + _apply_on = "sale.order" + + def on_compute_woocommerce_order_state(self, records, fields=None): + self.export_records(records, fields) + + def export_records(self, records, fields=None): + fields_to_update = { + "woocommerce_order_state", + } + for rec in records: + if rec.woocommerce_bind_ids: + if set(fields) & fields_to_update: + if records.sudo().woocommerce_bind_ids: + backends = records.sudo().woocommerce_bind_ids.backend_id + else: + backends = self.env["woocommerce.backend"].search( + [("state", "=", "validated")] + ) + Order = self.env["woocommerce.sale.order"] + domain = [ + *Order._get_base_domain(), + ("id", "in", records.ids), + ] + for backend in backends: + Order.export_batch(backend, domain=domain, delayed=False) diff --git a/connector_woocommerce/models/sale_order/sale_order.py b/connector_woocommerce/models/sale_order/sale_order.py new file mode 100644 index 000000000..ae516ca66 --- /dev/null +++ b/connector_woocommerce/models/sale_order/sale_order.py @@ -0,0 +1,106 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.sale.order", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) + is_woocommerce = fields.Boolean( + default=False, + ) + woocommerce_status_write_date = fields.Datetime( + compute="_compute_woocommerce_status_write_date", + store=True, + ) + + @api.depends("state", "picking_ids", "picking_ids.state") + def _compute_woocommerce_status_write_date(self): + for rec in self: + if rec.is_woocommerce: + rec.woocommerce_status_write_date = fields.Datetime.now() + + woocommerce_order_state = fields.Selection( + compute="_compute_woocommerce_order_state", + store=True, + selection=[ + ("processing", "Processing"), + ("done", "Done"), + ("cancel", "Cancel"), + ], + ) + + # TODO: REVIEW: try to do it without frozenset + def _get_woocommerce_order_state_mapping(self): + return { + frozenset(["cancel"]): "cancel", + frozenset(["done"]): "done", + frozenset(["processing"]): "processing", + frozenset(["cancel", "done"]): "done", + frozenset(["cancel", "processing"]): "processing", + frozenset(["done", "processing"]): "processing", + frozenset(["cancel", "done", "processing"]): "processing", + } + + @api.depends( + "state", + "order_line.qty_delivered", + "order_line.product_uom_qty", + "woocommerce_bind_ids", + ) + def _compute_woocommerce_order_state(self): + woocommerce_order_state_mapping = self._get_woocommerce_order_state_mapping() + # TODO: REVIEW: When module is intalled in large db, a memory error is raised + # this is the reason of set value just when record has binding. + # Try to avoid compute on install. + for rec in self: + if rec.is_woocommerce: + old_state = rec.woocommerce_order_state + lines_states = frozenset( + rec.order_line.filtered( + lambda x: x.product_id.product_tmpl_id.service_policy + != "ordered_timesheet" + ).mapped("woocommerce_order_line_state") + ) + if lines_states: + new_state = woocommerce_order_state_mapping[lines_states] + else: + new_state = "processing" + if not old_state: + rec.woocommerce_order_state = new_state + elif old_state != new_state: + rec.woocommerce_order_state = new_state + self._event("on_compute_woocommerce_order_state").notify( + rec, fields={"woocommerce_order_state"} + ) + # + # if not rec.woocommerce_bind_ids: + # rec.woocommerce_order_state = False + # else: + # old_state = rec.woocommerce_order_state + # lines_states = frozenset( + # rec.order_line.mapped("woocommerce_order_line_state") + # ) + # if lines_states: + # new_state = woocommerce_order_state_mapping[lines_states] + # else: + # new_state = "processing" + # if old_state != new_state: + # rec.woocommerce_order_state = new_state + # self._event("on_compute_woocommerce_order_state").notify( + # rec, fields={"woocommerce_order_state"} + # ) + + def action_confirm(self): + res = super().action_confirm() + if self.woocommerce_bind_ids.woocommerce_status == "on-hold": + self._event("on_compute_woocommerce_order_state").notify( + self, fields={"woocommerce_order_state"} + ) + return res diff --git a/connector_woocommerce/models/sale_order_line/__init__.py b/connector_woocommerce/models/sale_order_line/__init__.py new file mode 100644 index 000000000..1eda1882c --- /dev/null +++ b/connector_woocommerce/models/sale_order_line/__init__.py @@ -0,0 +1,4 @@ +from . import import_mapper +from . import binder +from . import binding +from . import sale_order_line diff --git a/connector_woocommerce/models/sale_order_line/binder.py b/connector_woocommerce/models/sale_order_line/binder.py new file mode 100644 index 000000000..a53e1d724 --- /dev/null +++ b/connector_woocommerce/models/sale_order_line/binder.py @@ -0,0 +1,38 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class SaleOrderLineBinder(Component): + _name = "woocommerce.sale.order.line.binder" + _inherit = "woocommerce.binder" + + _apply_on = "woocommerce.sale.order.line" + + external_id = ["id", "order_id"] + internal_id = [ + "woocommerce_order_line_id", + "woocommerce_sale_order_id", + ] + # internal_alt_id = ["product_id", "price_unit", "product_uom_qty", "order_id"] + + # TODO: REVIEW: Make an heuristic to bind sale order + # lines without binding but with a sale order binded + # def _get_internal_record_alt(self, values): + # model_name = self.unwrap_model() + # product = self.env['product.product'].search([('name', '=', values.pop('name'))]) + # if not product: + # return self.env[model_name] + # values['product_id'] = product.product_variant_ids.ids + # order = self.env['sale.order'].search( + # [('client_order_ref', '=', values.get('order_id'))]) + # if not order: + # return self.env[model_name] + # values['order_id'] = order.id + # line = super()._get_internal_record_alt(values) + # if line: + # if len(line) > 1: + # raise Exception("More than one sale order line found + # for this order, it's not possible to bind it") + # return line diff --git a/connector_woocommerce/models/sale_order_line/binding.py b/connector_woocommerce/models/sale_order_line/binding.py new file mode 100644 index 000000000..014efeaff --- /dev/null +++ b/connector_woocommerce/models/sale_order_line/binding.py @@ -0,0 +1,60 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class SaleOrderLineBinding(models.Model): + _name = "woocommerce.sale.order.line" + _inherit = "woocommerce.binding" + _inherits = {"sale.order.line": "odoo_id"} + + odoo_id = fields.Many2one( + comodel_name="sale.order.line", + string="Order line", + required=True, + ondelete="cascade", + ) + backend_id = fields.Many2one( + related="woocommerce_order_id.backend_id", + string="Backend", + readonly=True, + store=True, + required=False, + ) + woocommerce_order_id = fields.Many2one( + comodel_name="woocommerce.sale.order", + string="WooCommerce Order", + required=True, + ondelete="cascade", + index=True, + ) + woocommerce_sale_order_id = fields.Integer() + woocommerce_order_line_id = fields.Integer( + string="WooCommerce Order Line ID", + required=True, + ) + + _sql_constraints = [ + ( + "lol_ext_uniq", + "unique(backend_id, woocommerce_order_line_id)", + "A binding already exists with the same External (WooCommerce) ID.", + ), + ] + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + woocommerce_order_id = vals["woocommerce_order_id"] + binding = self.env["woocommerce.sale.order"].browse(woocommerce_order_id) + vals["order_id"] = binding.odoo_id.id + return super().create(vals_list) + # FIXME triggers function field + # The amounts (amount_total, ...) computed fields on 'sale.order' are + # not triggered when magento.sale.order.line are created. + # It might be a v8 regression, because they were triggered in + # v7. Before getting a better correction, force the computation + # by writing again on the line. + # line = binding.odoo_id + # line.write({'price_unit': line.price_unit}) diff --git a/connector_woocommerce/models/sale_order_line/import_mapper.py b/connector_woocommerce/models/sale_order_line/import_mapper.py new file mode 100644 index 000000000..b657fe2d3 --- /dev/null +++ b/connector_woocommerce/models/sale_order_line/import_mapper.py @@ -0,0 +1,108 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create + + +class SaleOrderLineImportMapper(Component): + _name = "woocommerce.sale.order.line.import.mapper" + _inherit = "woocommerce.import.mapper" + + _apply_on = "woocommerce.sale.order.line" + + @only_create + @mapping + def backend_id(self, record): + return {"backend_id": self.backend_record.id} + + @mapping + def woocommerce_line_id(self, record): + binder = self.binder_for() + external_id = binder.dict2id(record, in_field=False) + return binder.id2dict(external_id, in_field=True) + + @mapping + def price_unit(self, record): + return {"price_unit": (float(record["subtotal"])) / record["quantity"]} + + @mapping + def price_total(self, record): + return {"price_total": float(record["subtotal"])} + + @mapping + def price_tax(self, record): + return {"price_tax": float(record["total_tax"])} + + @mapping + def price_subtotal(self, record): + return {"price_subtotal": float(record["total"])} + + @mapping + def product_id(self, record): + if record["is_shipping"]: + shipping_product = self.backend_record.shipping_product_id + if not shipping_product: + raise ValidationError( + _("Shipping product not found, please define it on Backend") + ) + return {"product_id": shipping_product.id} + if record.get("variation_id"): + external_id = [record["product_id"], record["variation_id"]] + binder = self.binder_for("woocommerce.product.product") + product_odoo = binder.to_internal(external_id, unwrap=True) + else: + if not record.get("product_id"): + raise ValidationError( + _( + "Product not found in order line. " + "Probably this product has been deleted in WooCommerce." + ) + ) + external_id = [record["product_id"]] + binder = self.binder_for("woocommerce.product.template") + product_tmpl = binder.to_internal(external_id, unwrap=True) + product_odoo = product_tmpl.product_variant_id + assert product_odoo, ( + "product_id %s should have been imported in " + "SaleOrderImporter._import_dependencies" % (external_id,) + ) + return {"product_id": product_odoo.id} + + @mapping + def tax_id(self, record): + if record.get("taxes"): + taxes = [] + for tax in record.get("taxes"): + if tax["total"]: + tax_map = self.backend_record.tax_map_ids.filtered( + lambda x: tax["id"] == int(x.woocommerce_tax_rate_id) + ) + if not tax_map: + raise ValidationError( + _("Tax rate %s not found in backend mapping.") % tax["id"] + ) + else: + taxes.append(tax_map.account_tax.id) + if taxes: + return {"tax_id": [(6, 0, taxes)]} + + @mapping + def quantity(self, record): + return {"product_uom_qty": record["quantity"]} + + @mapping + def order(self, record): + external_id = [record["order_id"]] + binder = self.binder_for("woocommerce.sale.order") + order = binder.to_internal(external_id, unwrap=True) + return {"order_id": order} + + @mapping + def woocommerce_discount(self, record): + return { + "woocommerce_discount": record["discount"], + "discount": record["discount"], + } diff --git a/connector_woocommerce/models/sale_order_line/sale_order_line.py b/connector_woocommerce/models/sale_order_line/sale_order_line.py new file mode 100644 index 000000000..1e9406692 --- /dev/null +++ b/connector_woocommerce/models/sale_order_line/sale_order_line.py @@ -0,0 +1,181 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.tools.float_utils import float_round + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + # TODO: check lotes de facturacion -- no se como deberia verse, + # en las que he hecho sale un error de que no hay factura + + # This field is created with digits=False to force + # the creation with type numeric, like discount. + woocommerce_discount = fields.Float( + digits=False, + ) + stock_move_ids = fields.One2many( + comodel_name="stock.move", + inverse_name="sale_line_id", + ) + discount = fields.Float( + digits=False, + ) + woocommerce_order_line_state = fields.Selection( + compute="_compute_woocommerce_order_line_state", + selection=[ + ("processing", "Processing"), + ("done", "Done"), + ("cancel", "Cancel"), + ], + ) + + def _get_woocommerce_order_line_state_mapping(self): + return { + ("draft"): "processing", + ("sent"): "processing", + ("sale"): "processing", + ("done"): "processing", + ("cancel"): "cancel", + ("sent", "cancel"): "done", + ("sale", "cancel"): "done", + ("done", "cancel"): "done", + ("draft", "done", "done"): "done", + ("sent", "done", "done"): "done", + ("sale", "done", "done"): "done", + ("done", "done", "done"): "done", + } + + def _get_picking_state(self, picking_states): + pending_states = {"draft", "waiting", "confirmed", "assigned"} + for state in picking_states: + if state in pending_states: + return state + if "done" in picking_states: + return "done" + return "cancel" + + def _get_move_state(self, move_states): + pending_states = { + "draft", + "waiting", + "confirmed", + "assigned", + "partially_available", + } + for state in move_states: + if state in pending_states: + return state + return "done" + + @api.depends( + "order_id.state", + "order_id.picking_ids", + "stock_move_ids.quantity_done", + "order_id.picking_ids.state", + "product_uom_qty", + "qty_delivered", + ) + def _compute_woocommerce_order_line_state(self): + # TODO: add woocommerce status: delivered to workflow + woocommerce_order_line_state_mapping = ( + self._get_woocommerce_order_line_state_mapping() + ) + for rec in self: + rec.woocommerce_order_line_state = woocommerce_order_line_state_mapping[ + rec.order_id.state + ] + if rec.product_id.type == "service": + if all( + [ + rec.order_id.state in ("sale", "done"), + rec.product_id.product_tmpl_id.service_policy + in ("delivered_manual", "delivered_timesheet"), + rec.qty_delivered >= rec.product_uom_qty, + ] + ): + rec.woocommerce_order_line_state = "done" + elif ( + rec.product_id.product_tmpl_id.service_policy == "ordered_timesheet" + ): + rec.woocommerce_order_line_state = "done" + else: + rec.woocommerce_order_line_state = "processing" + else: + if ( + rec.order_id.picking_ids + and woocommerce_order_line_state_mapping.get( + ( + rec.order_id.state, + self._get_picking_state( + set(rec.order_id.picking_ids.mapped("state")) + ), + ) + ) + ): + rec.woocommerce_order_line_state = ( + woocommerce_order_line_state_mapping[ + ( + rec.order_id.state, + self._get_picking_state( + set(rec.order_id.picking_ids.mapped("state")) + ), + ) + ] + ) + if ( + rec.order_id.picking_ids + and rec.stock_move_ids + and woocommerce_order_line_state_mapping.get( + ( + rec.order_id.state, + self._get_picking_state( + set(rec.order_id.picking_ids.mapped("state")) + ), + self._get_move_state( + set(rec.stock_move_ids.mapped("state")) + ), + ) + ) + ): + rec.woocommerce_order_line_state = ( + woocommerce_order_line_state_mapping[ + ( + rec.order_id.state, + self._get_picking_state( + set(rec.order_id.picking_ids.mapped("state")) + ), + self._get_move_state( + set(rec.stock_move_ids.mapped("state")) + ), + ) + ] + ) + + woocommerce_bind_ids = fields.One2many( + comodel_name="woocommerce.sale.order.line", + inverse_name="odoo_id", + string="WooCommerce Bindings", + ) + + def write(self, vals): + prec = self.env.ref("product.decimal_discount").digits + if "woocommerce_discount" in vals: + vals["discount"] = vals["woocommerce_discount"] + elif "discount" in vals and not self.woocommerce_discount: + vals["discount"] = float_round(vals["discount"], precision_digits=prec) + return super().write(vals) + + @api.model_create_multi + def create(self, vals_list): + prec = self.env.ref("product.decimal_discount").digits + for values in vals_list: + if "woocommerce_discount" in values: + values["discount"] = values["woocommerce_discount"] + elif "discount" in values: + values["discount"] = float_round( + values["discount"], precision_digits=prec + ) + return super().create(vals_list) diff --git a/connector_woocommerce/security/connector_woocommerce.xml b/connector_woocommerce/security/connector_woocommerce.xml new file mode 100644 index 000000000..b00e221a8 --- /dev/null +++ b/connector_woocommerce/security/connector_woocommerce.xml @@ -0,0 +1,13 @@ + + + + + Connector Woocommerce backend multi-company rule + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + diff --git a/connector_woocommerce/security/ir.model.access.csv b/connector_woocommerce/security/ir.model.access.csv new file mode 100644 index 000000000..290acb674 --- /dev/null +++ b/connector_woocommerce/security/ir.model.access.csv @@ -0,0 +1,27 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +"access_woocommerce_backend_manager","woocommerce_backend connector manager","model_woocommerce_backend","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_backend_user","woocommerce_backend connector user","model_woocommerce_backend","base.group_user",1,0,0,0 +"access_woocommerce_backend_account_tax_manager","woocommerce_backend_account_tax connector manager","model_woocommerce_backend_account_tax","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_backend_account_tax_user","woocommerce_backend_account_tax connector user","model_woocommerce_backend_account_tax","base.group_user",1,0,0,0 +"access_woocommerce_backend_tax_class_manager","woocommerce_backend_tax_class connector manager","model_woocommerce_backend_tax_class","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_backend_tax_class_user","woocommerce_backend_tax_class connector user","model_woocommerce_backend_tax_class","base.group_user",1,0,0,0 +"access_woocommerce_backend_payment_mode_manager","woocommerce_backend_payment_mode connector manager","model_woocommerce_backend_payment_mode","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_backend_payment_mode_user","woocommerce_backend_payment_mode connector user","model_woocommerce_backend_payment_mode","base.group_user",1,0,0,0 +"access_woocommerce_product_product_manager","woocommerce_product_product connector manager","model_woocommerce_product_product","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_product_product_user","woocommerce_product_product connector user","model_woocommerce_product_product","base.group_user",1,0,0,0 +"access_woocommerce_product_public_category_manager","woocommerce_product_public_category connector manager","model_woocommerce_product_public_category","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_product_public_category_user","woocommerce_product_public_category connector user","model_woocommerce_product_public_category","base.group_user",1,0,0,0 +"access_woocommerce_product_template_manager","woocommerce_product_template connector manager","model_woocommerce_product_template","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_product_template_user","woocommerce_product_template connector user","model_woocommerce_product_template","base.group_user",1,0,0,0 +"access_woocommerce_product_attribute_manager","woocommerce_product_attribute connector manager","model_woocommerce_product_attribute","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_product_attribute_user","woocommerce_product_attribute connector user","model_woocommerce_product_attribute","base.group_user",1,0,0,0 +"access_woocommerce_product_attribute_value_manager","woocommerce_product_attribute_value connector manager","model_woocommerce_product_attribute_value","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_product_attribute_value_user","woocommerce_product_attribute_value connector user","model_woocommerce_product_attribute_value","base.group_user",1,0,0,0 +"access_woocommerce_sale_order_manager","woocommerce_sale_order connector manager","model_woocommerce_sale_order","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_sale_order_user","woocommerce_sale_order connector user","model_woocommerce_sale_order","base.group_user",1,0,0,0 +"access_woocommerce_res_partner_manager","woocommerce_res_partner connector manager","model_woocommerce_res_partner","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_res_partner_user","woocommerce_res_partner connector user","model_woocommerce_res_partner","base.group_user",1,0,0,0 +"access_woocommerce_sale_order_line_manager","woocommerce_sale_order_line connector manager","model_woocommerce_sale_order_line","connector.group_connector_manager",1,1,1,1 +"access_woocommerce_sale_order_line_user","woocommerce_sale_order_line connector user","model_woocommerce_sale_order_line","base.group_user",1,0,0,0 +"access_product_attachment_manager","product_attachment connector manager","model_product_attachment","connector.group_connector_manager",1,1,1,1 +"access_product_attachment_user","product_attachment connector user","model_product_attachment","base.group_user",1,0,0,0 diff --git a/connector_woocommerce/static/description/icon.png b/connector_woocommerce/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd641e792c30455187ca30940bc0f329ce8bbb0 GIT binary patch literal 6342 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 literal 0 HcmV?d00001 diff --git a/connector_woocommerce/views/connector_woocommerce_menu.xml b/connector_woocommerce/views/connector_woocommerce_menu.xml new file mode 100644 index 000000000..53d33afbc --- /dev/null +++ b/connector_woocommerce/views/connector_woocommerce_menu.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/connector_woocommerce/views/product_attribute.xml b/connector_woocommerce/views/product_attribute.xml new file mode 100644 index 000000000..46f436d53 --- /dev/null +++ b/connector_woocommerce/views/product_attribute.xml @@ -0,0 +1,60 @@ + + + + + product.attribute.connector.form + product.attribute + + + + + + + + + + + + + + woocommerce.product.attribute.view.form + woocommerce.product.attribute + +
+ + + + + + +
+
+
+ + woocommerce.product.attribute.view.tree + woocommerce.product.attribute + + + + + + +