From 59527423ace1f07c4d9fa27c1fbe6f80cc103e02 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Tue, 3 Oct 2017 16:06:37 +0200 Subject: [PATCH 01/16] [MIG] Add metafiles [skip ci] --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index d2c6edb..58422e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,15 @@ python: - "3.5" env: +<<<<<<< HEAD - VERSION="12.0" LINT_CHECK="1" - VERSION="12.0" ODOO_REPO="OCA/OCB" LINT_CHECK="0" INCLUDE="connector_woocommerce" - VERSION="12.0" ODOO_REPO="odoo/odoo" LINT_CHECK="0" INCLUDE="connector_woocommerce" +======= + - VERSION="11.0" LINT_CHECK="1" + - VERSION="11.0" ODOO_REPO="OCA/OCB" LINT_CHECK="0" INCLUDE="connector_woocommerce" + - VERSION="11.0" ODOO_REPO="odoo/odoo" LINT_CHECK="0" INCLUDE="connector_woocommerce" +>>>>>>> 2b50fc4... [MIG] Add metafiles From f46cb73cce0a13293feb11afda74ed8d5d9dcc16 Mon Sep 17 00:00:00 2001 From: Parthiv Patel Date: Fri, 28 Aug 2015 19:17:09 -0700 Subject: [PATCH 02/16] [PATCH 1/5] [RENAME] module renamed from woocommerceerpconnect to connector_woocommerce as mentioned in issue #3 --- connector_woocommerce/__init__.py | 25 ++ connector_woocommerce/__openerp__.py | 43 ++ connector_woocommerce/backend.py | 28 ++ connector_woocommerce/connector.py | 85 ++++ connector_woocommerce/model/__init__.py | 26 ++ connector_woocommerce/model/backend.py | 192 +++++++++ connector_woocommerce/model/customer.py | 229 +++++++++++ connector_woocommerce/model/product.py | 330 +++++++++++++++ .../model/product_category.py | 197 +++++++++ connector_woocommerce/model/sale.py | 377 ++++++++++++++++++ connector_woocommerce/related_action.py | 56 +++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 12004 bytes connector_woocommerce/unit/__init__.py | 25 ++ connector_woocommerce/unit/backend_adapter.py | 224 +++++++++++ connector_woocommerce/unit/binder.py | 163 ++++++++ .../unit/import_synchronizer.py | 284 +++++++++++++ connector_woocommerce/unit/mapper.py | 32 ++ connector_woocommerce/views/backend_view.xml | 95 +++++ 19 files changed, 2414 insertions(+) create mode 100755 connector_woocommerce/__init__.py create mode 100755 connector_woocommerce/__openerp__.py create mode 100755 connector_woocommerce/backend.py create mode 100755 connector_woocommerce/connector.py create mode 100755 connector_woocommerce/model/__init__.py create mode 100755 connector_woocommerce/model/backend.py create mode 100755 connector_woocommerce/model/customer.py create mode 100755 connector_woocommerce/model/product.py create mode 100755 connector_woocommerce/model/product_category.py create mode 100755 connector_woocommerce/model/sale.py create mode 100755 connector_woocommerce/related_action.py create mode 100755 connector_woocommerce/security/ir.model.access.csv create mode 100644 connector_woocommerce/static/description/icon.png create mode 100755 connector_woocommerce/unit/__init__.py create mode 100755 connector_woocommerce/unit/backend_adapter.py create mode 100755 connector_woocommerce/unit/binder.py create mode 100755 connector_woocommerce/unit/import_synchronizer.py create mode 100755 connector_woocommerce/unit/mapper.py create mode 100755 connector_woocommerce/views/backend_view.xml diff --git a/connector_woocommerce/__init__.py b/connector_woocommerce/__init__.py new file mode 100755 index 0000000..56fa5e3 --- /dev/null +++ b/connector_woocommerce/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from . import model +from . import backend +from . import connector +from . import related_action diff --git a/connector_woocommerce/__openerp__.py b/connector_woocommerce/__openerp__.py new file mode 100755 index 0000000..45598bf --- /dev/null +++ b/connector_woocommerce/__openerp__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +{ + 'name': 'WooCommerce Connector', + 'version': '1.0', + 'category': 'customized', + 'description': """WooCommerce Connector.""", + 'author': 'Tech Receptives', + 'maintainer': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['base', 'connector', 'connector_ecommerce'], + 'installable': True, + 'auto_install': False, + 'data': [ + "security/ir.model.access.csv", + "views/backend_view.xml", + ], + 'external_dependencies': { + 'python': ['woocommerce'], + }, + 'js': [], + 'application': True, + "sequence": 3, +} diff --git a/connector_woocommerce/backend.py b/connector_woocommerce/backend.py new file mode 100755 index 0000000..b86e7a1 --- /dev/null +++ b/connector_woocommerce/backend.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import openerp.addons.connector.backend as backend + +woo = backend.Backend('woo') +""" Generic woo Backend """ + +woov2 = backend.Backend(parent=woo, version='v2') +""" WooCommerce Backend for version v2 """ diff --git a/connector_woocommerce/connector.py b/connector_woocommerce/connector.py new file mode 100755 index 0000000..960a8d9 --- /dev/null +++ b/connector_woocommerce/connector.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from openerp import models, fields +from openerp.addons.connector.connector import (ConnectorEnvironment, + install_in_connector) +from openerp.addons.connector.checkpoint import checkpoint + +install_in_connector() + + +def get_environment(session, model_name, backend_id): + """ Create an environment to work with. """ + backend_record = session.env['wc.backend'].browse(backend_id) + env = ConnectorEnvironment(backend_record, session, model_name) + lang = backend_record.default_lang_id + lang_code = lang.code if lang else 'en_US' + if lang_code == session.context.get('lang'): + return env + else: + with env.session.change_context(lang=lang_code): + return env + + +class WooBinding(models.AbstractModel): + + """ Abstract Model for the Bindigs. + + All the models used as bindings between WooCommerce and OpenERP + (``woo.res.partner``, ``woo.product.product``, ...) should + ``_inherit`` it. + """ + _name = 'woo.binding' + _inherit = 'external.binding' + _description = 'Woo Binding (abstract)' + + # openerp_id = openerp-side id must be declared in concrete model + backend_id = fields.Many2one( + comodel_name='wc.backend', + string='Woo Backend', + required=True, + ondelete='restrict', + ) + # fields.Char because 0 is a valid WooCommerce ID + woo_id = fields.Char(string='ID on Woo') + + _sql_constraints = [ + ('woo_uniq', 'unique(backend_id, woo_id)', + 'A binding already exists with the same Woo ID.'), + ] + + +def add_checkpoint(session, model_name, record_id, backend_id): + """ Add a row in the model ``connector.checkpoint`` for a record, + meaning it has to be reviewed by a user. + + :param session: current session + :type session: :class:`openerp.addons.connector.session.ConnectorSession` + :param model_name: name of the model of the record to be reviewed + :type model_name: str + :param record_id: ID of the record to be reviewed + :type record_id: int + :param backend_id: ID of the WooCommerce Backend + :type backend_id: int + """ + return checkpoint.add_checkpoint(session, model_name, record_id, + 'wc.backend', backend_id) diff --git a/connector_woocommerce/model/__init__.py b/connector_woocommerce/model/__init__.py new file mode 100755 index 0000000..d87e529 --- /dev/null +++ b/connector_woocommerce/model/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from . import backend +from . import product_category +from . import product +from . import customer +from . import sale diff --git a/connector_woocommerce/model/backend.py b/connector_woocommerce/model/backend.py new file mode 100755 index 0000000..996c7c5 --- /dev/null +++ b/connector_woocommerce/model/backend.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from openerp import models, api, fields, _ +from woocommerce import API +from openerp.exceptions import Warning +from openerp.addons.connector.session import ConnectorSession +from datetime import datetime +from .product_category import category_import_batch +from .product import product_import_batch +from .customer import customer_import_batch +from .sale import sale_order_import_batch + + +class wc_backend(models.Model): + _name = 'wc.backend' + _inherit = 'connector.backend' + _description = 'WooCommerce Backend Configuration' + name = fields.Char(string='name') + _backend_type = 'woo' + location = fields.Char("Url") + consumer_key = fields.Char("Consumer key") + consumer_secret = fields.Char("Consumer Secret") + version = fields.Selection([('v2', 'V2')], 'Version') + verify_ssl = fields.Boolean("Verify SSL") + default_lang_id = fields.Many2one( + comodel_name='res.lang', + string='Default Language', + help="If a default language is selected, the records " + "will be imported in the translation of this language.\n" + "Note that a similar configuration exists " + "for each storeview.", + ) + + @api.multi + def get_product_ids(self, data): + product_ids = [x['id'] for x in data['products']] + product_ids = sorted(product_ids) + return product_ids + + @api.multi + def get_product_category_ids(self, data): + product_category_ids = [x['id'] for x in data['product_categories']] + product_category_ids = sorted(product_category_ids) + return product_category_ids + + @api.multi + def get_customer_ids(self, data): + customer_ids = [x['id'] for x in data['customers']] + customer_ids = sorted(customer_ids) + return customer_ids + + @api.multi + def get_order_ids(self, data): + order_ids = self.check_existing_order(data) + return order_ids + + @api.multi + def update_existing_order(self, woo_sale_order, data): + """ Enter Your logic for Existing Sale Order """ + return True + + @api.multi + def check_existing_order(self, data): + order_ids = [] + for val in data['orders']: + woo_sale_order = self.env['woo.sale.order'].search( + [('woo_id', '=', val['id'])]) + if woo_sale_order: + self.update_existing_order(woo_sale_order[0], val) + continue + order_ids.append(val['id']) + return order_ids + + @api.multi + def test_connection(self): + location = self.location + cons_key = self.consumer_key + sec_key = self.consumer_secret + version = 'v2' + + wcapi = API(url=location, consumer_key=cons_key, + consumer_secret=sec_key, version=version) + r = wcapi.get("products") + if r.status_code == 404: + raise Warning(_("Enter Valid url")) + val = r.json() + msg = '' + if 'errors' in r.json(): + msg = val['errors'][0]['message'] + '\n' + val['errors'][0]['code'] + raise Warning(_(msg)) + else: + raise Warning(_('Test Success')) + return True + + @api.multi + def import_category(self): + session = ConnectorSession(self.env.cr, self.env.uid, + context=self.env.context) + import_start_time = datetime.now() + backend_id = self.id + from_date = None + category_import_batch.delay( + session, 'woo.product.category', backend_id, + {'from_date': from_date, + 'to_date': import_start_time}, priority=1) + return True + + @api.multi + def import_product(self): + session = ConnectorSession(self.env.cr, self.env.uid, + context=self.env.context) + import_start_time = datetime.now() + backend_id = self.id + from_date = None + product_import_batch.delay( + session, 'woo.product.product', backend_id, + {'from_date': from_date, + 'to_date': import_start_time}, priority=2) + return True + + @api.multi + def import_customer(self): + session = ConnectorSession(self.env.cr, self.env.uid, + context=self.env.context) + import_start_time = datetime.now() + backend_id = self.id + from_date = None + customer_import_batch.delay( + session, 'woo.res.partner', backend_id, + {'from_date': from_date, + 'to_date': import_start_time}, priority=3) + return True + + @api.multi + def import_order(self): + session = ConnectorSession(self.env.cr, self.env.uid, + context=self.env.context) + import_start_time = datetime.now() + backend_id = self.id + from_date = None + sale_order_import_batch.delay( + session, 'woo.sale.order', backend_id, + {'from_date': from_date, + 'to_date': import_start_time}, priority=4) + return True + + @api.multi + def import_categories(self): + """ Import Product categories """ + for backend in self: + backend.import_category() + return True + + @api.multi + def import_products(self): + """ Import categories from all websites """ + for backend in self: + backend.import_product() + return True + + @api.multi + def import_customers(self): + """ Import categories from all websites """ + for backend in self: + backend.import_customer() + return True + + @api.multi + def import_orders(self): + """ Import Orders from all websites """ + for backend in self: + backend.import_order() + return True diff --git a/connector_woocommerce/model/customer.py b/connector_woocommerce/model/customer.py new file mode 100755 index 0000000..3f26f47 --- /dev/null +++ b/connector_woocommerce/model/customer.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import logging +import xmlrpclib +from openerp import models, fields +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.unit.mapper import (mapping, + ImportMapper + ) +from openerp.addons.connector.exception import IDMissingInBackend +from ..unit.backend_adapter import (GenericAdapter) +from ..unit.import_synchronizer import (DelayedBatchImporter, WooImporter) +from ..connector import get_environment +from ..backend import woo + +_logger = logging.getLogger(__name__) + + +class WooResPartner(models.Model): + _name = 'woo.res.partner' + _inherit = 'woo.binding' + _inherits = {'res.partner': 'openerp_id'} + _description = 'woo res partner' + + _rec_name = 'name' + + openerp_id = fields.Many2one(comodel_name='res.partner', + string='Partner', + required=True, + ondelete='cascade') + backend_id = fields.Many2one( + comodel_name='wc.backend', + string='Woo Backend', + store=True, + readonly=False, + ) + + +@woo +class CustomerAdapter(GenericAdapter): + _model_name = 'woo.res.partner' + _woo_model = 'customers' + + def _call(self, method, arguments): + try: + return super(CustomerAdapter, self)._call(method, arguments) + except xmlrpclib.Fault as err: + # this is the error in the WooCommerce API + # when the customer does not exist + if err.faultCode == 102: + raise IDMissingInBackend + else: + raise + + def search(self, filters=None, from_date=None, to_date=None): + """ Search records according to some criteria and return a + list of ids + + :rtype: list + """ + if filters is None: + filters = {} + WOO_DATETIME_FORMAT = '%Y/%m/%d %H:%M:%S' + dt_fmt = WOO_DATETIME_FORMAT + if from_date is not None: + # updated_at include the created records + filters.setdefault('updated_at', {}) + filters['updated_at']['from'] = from_date.strftime(dt_fmt) + if to_date is not None: + filters.setdefault('updated_at', {}) + filters['updated_at']['to'] = to_date.strftime(dt_fmt) + # the search method is on ol_customer instead of customer + return self._call('customers/list', + [filters] if filters else [{}]) + + +@woo +class CustomerBatchImporter(DelayedBatchImporter): + + """ Import the WooCommerce Partners. + + For every partner in the list, a delayed job is created. + """ + _model_name = ['woo.res.partner'] + + def _import_record(self, woo_id, priority=None): + """ Delay a job for the import """ + super(CustomerBatchImporter, self)._import_record( + woo_id, priority=priority) + + def run(self, filters=None): + """ Run the synchronization """ + from_date = filters.pop('from_date', None) + to_date = filters.pop('to_date', None) + record_ids = self.backend_adapter.search( + filters, + from_date=from_date, + to_date=to_date, + ) +# record_ids = self.env['wc.backend'].get_customer_ids(record_ids) + _logger.info('search for woo partners %s returned %s', + filters, record_ids) + for record_id in record_ids: + self._import_record(record_id, 40) + + +CustomerBatchImporter = CustomerBatchImporter # deprecated + + +@woo +class CustomerImporter(WooImporter): + _model_name = ['woo.res.partner'] + + def _import_dependencies(self): + """ Import the dependencies for the record""" + return + + def _create(self, data): + openerp_binding = super(CustomerImporter, self)._create(data) + return openerp_binding + + def _after_import(self, binding): + """ Hook called at the end of the import """ + return + +CustomerImport = CustomerImporter # deprecated + + +@woo +class CustomerImportMapper(ImportMapper): + _model_name = 'woo.res.partner' + + @mapping + def name(self, record): + if record['customer']: + rec = record['customer'] + return {'name': rec['first_name'] + " " + rec['last_name']} + + @mapping + def email(self, record): + if record['customer']: + rec = record['customer'] + return {'email': rec['email'] or None} + + @mapping + def city(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + return {'city': rec['city'] or None} + + @mapping + def zip(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + return {'zip': rec['postcode'] or None} + + @mapping + def address(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + return {'street': rec['address_1'] or None} + + @mapping + def address_2(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + return {'street2': rec['address_2'] or None} + + @mapping + def country(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + if rec['country']: + country_id = self.env['res.country'].search( + [('code', '=', rec['country'])]) + country_id = country_id.id + else: + country_id = False + return {'country_id': country_id} + + @mapping + def state(self, record): + if record['customer']: + rec = record['customer']['billing_address'] + if rec['state'] and rec['country']: + state_id = self.env['res.country.state'].search( + [('code', '=', rec['state'])]) + if not state_id: + country_id = self.env['res.country'].search( + [('code', '=', rec['country'])]) + state_id = self.env['res.country.state'].create( + {'name': rec['state'], + 'code': rec['state'], + 'country_id': country_id.id}) + state_id = state_id.id or False + else: + state_id = False + return {'state_id': state_id} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@job(default_channel='root.woo') +def customer_import_batch(session, model_name, backend_id, filters=None): + """ Prepare the import of Customer """ + env = get_environment(session, model_name, backend_id) + importer = env.get_connector_unit(CustomerBatchImporter) + importer.run(filters=filters) diff --git a/connector_woocommerce/model/product.py b/connector_woocommerce/model/product.py new file mode 100755 index 0000000..ffe8a88 --- /dev/null +++ b/connector_woocommerce/model/product.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import logging +import urllib2 +import xmlrpclib +import base64 +from openerp import models, fields +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.exception import MappingError +from openerp.addons.connector.unit.synchronizer import (Importer, + ) +from openerp.addons.connector.unit.mapper import (mapping, + ImportMapper + ) +from openerp.addons.connector.exception import IDMissingInBackend +from ..unit.backend_adapter import (GenericAdapter) +from ..unit.import_synchronizer import (DelayedBatchImporter, WooImporter) +from ..connector import get_environment +from ..backend import woo + +_logger = logging.getLogger(__name__) + + +class WooProductProduct(models.Model): + _name = 'woo.product.product' + _inherit = 'woo.binding' + _inherits = {'product.product': 'openerp_id'} + _description = 'woo product product' + + _rec_name = 'name' + openerp_id = fields.Many2one(comodel_name='product.product', + string='product', + required=True, + ondelete='cascade') + backend_id = fields.Many2one( + comodel_name='wc.backend', + string='Woo Backend', + store=True, + readonly=False, + required=True, + ) + + slug = fields.Char('Slung Name') + credated_at = fields.Date('created_at') + weight = fields.Float('weight') + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + woo_categ_ids = fields.Many2many( + comodel_name='product.category', + string='Woo product category', + ) + in_stock = fields.Boolean('In Stock') + + +@woo +class ProductProductAdapter(GenericAdapter): + _model_name = 'woo.product.product' + _woo_model = 'products/details' + + def _call(self, method, arguments): + try: + return super(ProductProductAdapter, self)._call(method, arguments) + except xmlrpclib.Fault as err: + # this is the error in the WooCommerce API + # when the customer does not exist + if err.faultCode == 102: + raise IDMissingInBackend + else: + raise + + def search(self, filters=None, from_date=None, to_date=None): + """ Search records according to some criteria and return a + list of ids + + :rtype: list + """ + if filters is None: + filters = {} + WOO_DATETIME_FORMAT = '%Y/%m/%d %H:%M:%S' + dt_fmt = WOO_DATETIME_FORMAT + if from_date is not None: + # updated_at include the created records + filters.setdefault('updated_at', {}) + filters['updated_at']['from'] = from_date.strftime(dt_fmt) + if to_date is not None: + filters.setdefault('updated_at', {}) + filters['updated_at']['to'] = to_date.strftime(dt_fmt) + + return self._call('products/list', + [filters] if filters else [{}]) + + def get_images(self, id, storeview_id=None): + return self._call('products/' + str(id), [int(id), storeview_id, 'id']) + + def read_image(self, id, image_name, storeview_id=None): + return self._call('products', + [int(id), image_name, storeview_id, 'id']) + + +@woo +class ProductBatchImporter(DelayedBatchImporter): + + """ Import the WooCommerce Partners. + + For every partner in the list, a delayed job is created. + """ + _model_name = ['woo.product.product'] + + def _import_record(self, woo_id, priority=None): + """ Delay a job for the import """ + super(ProductBatchImporter, self)._import_record( + woo_id, priority=priority) + + def run(self, filters=None): + """ Run the synchronization """ +# + from_date = filters.pop('from_date', None) + to_date = filters.pop('to_date', None) + record_ids = self.backend_adapter.search( + filters, + from_date=from_date, + to_date=to_date, + ) + _logger.info('search for woo Products %s returned %s', + filters, record_ids) + for record_id in record_ids: + self._import_record(record_id, 30) + + +ProductBatchImporter = ProductBatchImporter + + +@woo +class ProductProductImporter(WooImporter): + _model_name = ['woo.product.product'] + + def _import_dependencies(self): + """ Import the dependencies for the record""" + record = self.woo_record + record = record['product'] + for woo_category_id in record['categories']: + self._import_dependency(woo_category_id, + 'woo.product.category') + + def _create(self, data): + openerp_binding = super(ProductProductImporter, self)._create(data) + return openerp_binding + + def _after_import(self, binding): + """ Hook called at the end of the import """ + image_importer = self.unit_for(ProductImageImporter) + image_importer.run(self.woo_id, binding.id) + return + +ProductProductImport = ProductProductImporter + + +@woo +class ProductImageImporter(Importer): + + """ Import images for a record. + + Usually called from importers, in ``_after_import``. + For instance from the products importer. + """ + _model_name = ['woo.product.product', + ] + + def _get_images(self, storeview_id=None): + return self.backend_adapter.get_images(self.woo_id) + + def _sort_images(self, images): + """ Returns a list of images sorted by their priority. + An image with the 'image' type is the the primary one. + The other images are sorted by their position. + + The returned list is reversed, the items at the end + of the list have the higher priority. + """ + if not images: + return {} + # place the images where the type is 'image' first then + # sort them by the reverse priority (last item of the list has + # the the higher priority) + + def _get_binary_image(self, image_data): + url = image_data['src'].encode('utf8') + url = str(url).replace("\\", '') + try: + request = urllib2.Request(url) + binary = urllib2.urlopen(request) + except urllib2.HTTPError as err: + if err.code == 404: + # the image is just missing, we skip it + return + else: + # we don't know why we couldn't download the image + # so we propagate the error, the import will fail + # and we have to check why it couldn't be accessed + raise + else: + return binary.read() + + def run(self, woo_id, binding_id): + self.woo_id = woo_id + images = self._get_images() + images = images['product'] + images = images['images'] + binary = None + while not binary and images: + binary = self._get_binary_image(images.pop()) + if not binary: + return + model = self.model.with_context(connector_no_export=True) + binding = model.browse(binding_id) + binding.write({'image': base64.b64encode(binary)}) + + +@woo +class ProductProductImportMapper(ImportMapper): + _model_name = 'woo.product.product' + + direct = [ + ('description', 'description'), + ('weight', 'weight'), + ] + + @mapping + def is_active(self, record): + """Check if the product is active in Woo + and set active flag in OpenERP + status == 1 in Woo means active""" + if record['product']: + rec = record['product'] + return {'active': rec['visible']} + + @mapping + def in_stock(self, record): + if record['product']: + rec = record['product'] + return {'in_stock': rec['in_stock']} + + @mapping + def name(self, record): + if record['product']: + rec = record['product'] + return {'name': rec['title']} + + @mapping + def type(self, record): + if record['product']: + rec = record['product'] + if rec['type'] == 'simple': + return {'type': 'product'} + + @mapping + def categories(self, record): + if record['product']: + rec = record['product'] + woo_categories = rec['categories'] + binder = self.binder_for('woo.product.category') + category_ids = [] + main_categ_id = None + for woo_category_id in woo_categories: + cat_id = binder.to_openerp(woo_category_id, unwrap=True) + if cat_id is None: + raise MappingError("The product category with " + "woo id %s is not imported." % + woo_category_id) + category_ids.append(cat_id) + if category_ids: + main_categ_id = category_ids.pop(0) + result = {'woo_categ_ids': [(6, 0, category_ids)]} + if main_categ_id: # OpenERP assign 'All Products' if not specified + result['categ_id'] = main_categ_id + return result + + @mapping + def price(self, record): + """ The price is imported at the creation of + the product, then it is only modified and exported + from OpenERP """ + if record['product']: + rec = record['product'] + return {'list_price': rec and rec['price'] or 0.0} + + @mapping + def sale_price(self, record): + """ The price is imported at the creation of + the product, then it is only modified and exported + from OpenERP """ + if record['product']: + rec = record['product'] + return {'standard_price': rec and rec['sale_price'] or 0.0} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@job(default_channel='root.woo') +def product_import_batch(session, model_name, backend_id, filters=None): + """ Prepare the import of product modified on Woo """ + if filters is None: + filters = {} + env = get_environment(session, model_name, backend_id) + importer = env.get_connector_unit(ProductBatchImporter) + importer.run(filters=filters) diff --git a/connector_woocommerce/model/product_category.py b/connector_woocommerce/model/product_category.py new file mode 100755 index 0000000..c6fac06 --- /dev/null +++ b/connector_woocommerce/model/product_category.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import logging +import xmlrpclib +from openerp import models, fields +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.exception import MappingError +from openerp.addons.connector.unit.mapper import (mapping, + ImportMapper + ) +from openerp.addons.connector.exception import IDMissingInBackend +from ..unit.backend_adapter import (GenericAdapter) +from ..unit.import_synchronizer import (DelayedBatchImporter, WooImporter) +from ..connector import get_environment +from ..backend import woo +_logger = logging.getLogger(__name__) + + +class WooProductCategory(models.Model): + _name = 'woo.product.category' + _inherit = 'woo.binding' + _inherits = {'product.category': 'openerp_id'} + _description = 'woo product category' + + _rec_name = 'name' + + openerp_id = fields.Many2one(comodel_name='product.category', + string='category', + required=True, + ondelete='cascade') + backend_id = fields.Many2one( + comodel_name='wc.backend', + string='Woo Backend', + store=True, + readonly=False, + ) + + slug = fields.Char('Slung Name') + woo_parent_id = fields.Many2one( + comodel_name='woo.product.category', + string='Woo Parent Category', + ondelete='cascade',) + description = fields.Char('Description') + count = fields.Integer('count') + + +@woo +class CategoryAdapter(GenericAdapter): + _model_name = 'woo.product.category' + _woo_model = 'products/categories' + + def _call(self, method, arguments): + try: + return super(CategoryAdapter, self)._call(method, arguments) + except xmlrpclib.Fault as err: + # this is the error in the WooCommerce API + # when the customer does not exist + if err.faultCode == 102: + raise IDMissingInBackend + else: + raise + + def search(self, filters=None, from_date=None, to_date=None): + """ Search records according to some criteria and return a + list of ids + + :rtype: list + """ + if filters is None: + filters = {} + WOO_DATETIME_FORMAT = '%Y/%m/%d %H:%M:%S' + dt_fmt = WOO_DATETIME_FORMAT + if from_date is not None: + filters.setdefault('updated_at', {}) + filters['updated_at']['from'] = from_date.strftime(dt_fmt) + if to_date is not None: + filters.setdefault('updated_at', {}) + filters['updated_at']['to'] = to_date.strftime(dt_fmt) + return self._call('products/categories/list', + [filters] if filters else [{}]) + + +@woo +class CategoryBatchImporter(DelayedBatchImporter): + + """ Import the WooCommerce Partners. + + For every partner in the list, a delayed job is created. + """ + _model_name = ['woo.product.category'] + + def _import_record(self, woo_id, priority=None): + """ Delay a job for the import """ + + super(CategoryBatchImporter, self)._import_record( + woo_id, priority=priority) + + def run(self, filters=None): + """ Run the synchronization """ + from_date = filters.pop('from_date', None) + to_date = filters.pop('to_date', None) + record_ids = self.backend_adapter.search( + filters, + from_date=from_date, + to_date=to_date, + ) + _logger.info('search for woo Product Category %s returned %s', + filters, record_ids) + for record_id in record_ids: + self._import_record(record_id) +CategoryBatchImporter = CategoryBatchImporter + + +@woo +class ProductCategoryImporter(WooImporter): + _model_name = ['woo.product.category'] + + def _import_dependencies(self): + """ Import the dependencies for the record""" + record = self.woo_record + # import parent category + # the root category has a 0 parent_id + record = record['product_category'] + if record['parent']: + parent_id = record['parent'] + if self.binder.to_openerp(parent_id) is None: + importer = self.unit_for(WooImporter) + importer.run(parent_id) + return + + def _create(self, data): + openerp_binding = super(ProductCategoryImporter, self)._create(data) + return openerp_binding + + def _after_import(self, binding): + """ Hook called at the end of the import """ + return + +ProductCategoryImport = ProductCategoryImporter + + +@woo +class ProductCategoryImportMapper(ImportMapper): + _model_name = 'woo.product.category' + + @mapping + def name(self, record): + if record['product_category']: + rec = record['product_category'] + return {'name': rec['name']} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} +# + + @mapping + def parent_id(self, record): + if record['product_category']: + rec = record['product_category'] + if not rec['parent']: + return + binder = self.binder_for() + category_id = binder.to_openerp(rec['parent'], unwrap=True) + woo_cat_id = binder.to_openerp(rec['parent']) + if category_id is None: + raise MappingError("The product category with " + "woo id %s is not imported." % + rec['parent']) + return {'parent_id': category_id, 'woo_parent_id': woo_cat_id} + + +@job(default_channel='root.woo') +def category_import_batch(session, model_name, backend_id, filters=None): + """ Prepare the import of category modified on WooCommerce """ + env = get_environment(session, model_name, backend_id) + importer = env.get_connector_unit(CategoryBatchImporter) + importer.run(filters=filters) diff --git a/connector_woocommerce/model/sale.py b/connector_woocommerce/model/sale.py new file mode 100755 index 0000000..ae84aa8 --- /dev/null +++ b/connector_woocommerce/model/sale.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import logging +import xmlrpclib +from openerp import models, fields, api +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.unit.mapper import (mapping, + ImportMapper + ) +from openerp.addons.connector.exception import IDMissingInBackend +from ..unit.backend_adapter import (GenericAdapter) +from ..unit.import_synchronizer import (DelayedBatchImporter, WooImporter) +from ..connector import get_environment +from ..backend import woo +_logger = logging.getLogger(__name__) + + +class woo_sale_order_status(models.Model): + _name = 'woo.sale.order.status' + _description = 'WooCommerce Sale Order Status' + + name = fields.Char('Name') + desc = fields.Text('Description') + + +class SaleOrder(models.Model): + + _inherit = 'sale.order' + status_id = fields.Many2one('woo.sale.order.status', + 'WooCommerce Order Status') + + +class WooSaleOrder(models.Model): + _name = 'woo.sale.order' + _inherit = 'woo.binding' + _inherits = {'sale.order': 'openerp_id'} + _description = 'Woo Sale Order' + + _rec_name = 'name' + + status_id = fields.Many2one('woo.sale.order.status', + 'WooCommerce Order Status') + + openerp_id = fields.Many2one(comodel_name='sale.order', + string='Sale Order', + required=True, + ondelete='cascade') + woo_order_line_ids = fields.One2many( + comodel_name='woo.sale.order.line', + inverse_name='woo_order_id', + string='Woo Order Lines' + ) + backend_id = fields.Many2one( + comodel_name='wc.backend', + string='Woo Backend', + store=True, + readonly=False, + required=True, + ) + + +class WooSaleOrderLine(models.Model): + _name = 'woo.sale.order.line' + _inherits = {'sale.order.line': 'openerp_id'} + + woo_order_id = fields.Many2one(comodel_name='woo.sale.order', + string='Woo Sale Order', + required=True, + ondelete='cascade', + select=True) + + openerp_id = fields.Many2one(comodel_name='sale.order.line', + string='Sale Order Line', + required=True, + ondelete='cascade') + + backend_id = fields.Many2one( + related='woo_order_id.backend_id', + string='Woo Backend', + readonly=True, + store=True, + required=False, + ) + + @api.model + def create(self, vals): + woo_order_id = vals['woo_order_id'] + binding = self.env['woo.sale.order'].browse(woo_order_id) + vals['order_id'] = binding.openerp_id.id + binding = super(WooSaleOrderLine, self).create(vals) + return binding + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + woo_bind_ids = fields.One2many( + comodel_name='woo.sale.order.line', + inverse_name='openerp_id', + string="WooCommerce Bindings", + ) + + +@woo +class SaleOrderLineImportMapper(ImportMapper): + _model_name = 'woo.sale.order.line' + + direct = [('quantity', 'product_uom_qty'), + ('quantity', 'product_uos_qty'), + ('name', 'name'), + ('price', 'price_unit') + ] + + @mapping + def product_id(self, record): + binder = self.binder_for('woo.product.product') + product_id = binder.to_openerp(record['product_id'], unwrap=True) + assert product_id is not None, ( + "product_id %s should have been imported in " + "SaleOrderImporter._import_dependencies" % record['product_id']) + return {'product_id': product_id} + + +@woo +class SaleOrderAdapter(GenericAdapter): + _model_name = 'woo.sale.order' + _woo_model = 'orders' + + def _call(self, method, arguments): + try: + return super(SaleOrderAdapter, self)._call(method, arguments) + except xmlrpclib.Fault as err: + # this is the error in the Woo API + # when the customer does not exist + if err.faultCode == 102: + raise IDMissingInBackend + else: + raise + + def search(self, filters=None, from_date=None, to_date=None): + """ Search records according to some criteria and return a + list of ids + + :rtype: list + """ + if filters is None: + filters = {} + WOO_DATETIME_FORMAT = '%Y/%m/%d %H:%M:%S' + dt_fmt = WOO_DATETIME_FORMAT + if from_date is not None: + # updated_at include the created records + filters.setdefault('updated_at', {}) + filters['updated_at']['from'] = from_date.strftime(dt_fmt) + if to_date is not None: + filters.setdefault('updated_at', {}) + filters['updated_at']['to'] = to_date.strftime(dt_fmt) + + return self._call('orders/list', + [filters] if filters else [{}]) + + +@woo +class SaleOrderBatchImporter(DelayedBatchImporter): + + """ Import the WooCommerce Partners. + + For every partner in the list, a delayed job is created. + """ + _model_name = ['woo.sale.order'] + + def _import_record(self, woo_id, priority=None): + """ Delay a job for the import """ + super(SaleOrderBatchImporter, self)._import_record( + woo_id, priority=priority) + + def update_existing_order(self, woo_sale_order, record_id): + """ Enter Your logic for Existing Sale Order """ + return True + + def run(self, filters=None): + """ Run the synchronization """ +# + from_date = filters.pop('from_date', None) + to_date = filters.pop('to_date', None) + record_ids = self.backend_adapter.search( + filters, + from_date=from_date, + to_date=to_date, + ) + order_ids = [] + for record_id in record_ids: + woo_sale_order = self.env['woo.sale.order'].search( + [('woo_id', '=', record_id)]) + if woo_sale_order: + self.update_existing_order(woo_sale_order[0], record_id) + else: + order_ids.append(record_id) + _logger.info('search for woo partners %s returned %s', + filters, record_ids) + for record_id in order_ids: + self._import_record(record_id, 50) + + +SaleOrderBatchImporter = SaleOrderBatchImporter +# + + +@woo +class SaleOrderImporter(WooImporter): + _model_name = ['woo.sale.order'] + + def _import_addresses(self): + record = self.woo_record + record = record['order'] + self._import_dependency(record['customer_id'], + 'woo.res.partner') + + def _import_dependencies(self): + """ Import the dependencies for the record""" + record = self.woo_record + + self._import_addresses() + record = record['items'] + for line in record: + _logger.debug('line: %s', line) + if 'product_id' in line: + self._import_dependency(line['product_id'], + 'woo.product.product') + + def _clean_woo_items(self, resource): + """ + Method that clean the sale order line given by WooCommerce before + importing it + + This method has to stay here because it allow to customize the + behavior of the sale order. + + """ + child_items = {} # key is the parent item id + top_items = [] + + # Group the childs with their parent + for item in resource['order']['line_items']: + if item.get('parent_item_id'): + child_items.setdefault(item['parent_item_id'], []).append(item) + else: + top_items.append(item) + + all_items = [] + for top_item in top_items: + all_items.append(top_item) + resource['items'] = all_items + return resource + + def _create(self, data): + openerp_binding = super(SaleOrderImporter, self)._create(data) + return openerp_binding + + def _after_import(self, binding): + """ Hook called at the end of the import """ + return + + def _get_woo_data(self): + """ Return the raw WooCommerce data for ``self.woo_id`` """ + record = super(SaleOrderImporter, self)._get_woo_data() + # sometimes we need to clean woo items (ex : configurable + # product in a sale) + record = self._clean_woo_items(record) + return record +SaleOrderImport = SaleOrderImporter + + +@woo +class SaleOrderImportMapper(ImportMapper): + _model_name = 'woo.sale.order' + + children = [('items', 'woo_order_line_ids', 'woo.sale.order.line'), + ] + + @mapping + def status(self, record): + if record['order']: + rec = record['order'] + if rec['status']: + status_id = self.env['woo.sale.order.status'].search( + [('name', '=', rec['status'])]) + if status_id: + return {'status_id': status_id[0].id} + else: + status_id = self.env['woo.sale.order.status'].create({ + 'name': rec['status'] + }) + return {'status_id': status_id.id} + else: + return {'status_id': False} + + @mapping + def customer_id(self, record): + if record['order']: + rec = record['order'] + binder = self.binder_for('woo.res.partner') + if rec['customer_id']: + partner_id = binder.to_openerp(rec['customer_id'], + unwrap=True) or False +# customer_id = str(rec['customer_id']) + assert partner_id, ("Please Check Customer Role \ + in WooCommerce") + result = {'partner_id': partner_id} + onchange_val = self.env['sale.order'].onchange_partner_id( + partner_id) + result.update(onchange_val['value']) + else: + customer = rec['customer']['billing_address'] + country_id = False + state_id = False + if customer['country']: + country_id = self.env['res.country'].search( + [('code', '=', customer['country'])]) + if country_id: + country_id = country_id.id + if customer['state']: + state_id = self.env['res.country.state'].search( + [('code', '=', customer['state'])]) + if state_id: + state_id = state_id.id + name = customer['first_name'] + ' ' + customer['last_name'] + partner_dict = { + 'name': name, + 'city': customer['city'], + 'phone': customer['phone'], + 'zip': customer['postcode'], + 'state_id': state_id, + 'country_id': country_id + } + partner_id = self.env['res.partner'].create(partner_dict) + partner_dict.update({ + 'backend_id': self.backend_record.id, + 'openerp_id': partner_id.id, + }) +# woo_partner_id = self.env['woo.res.partner'].create( +# partner_dict) + result = {'partner_id': partner_id.id} + onchange_val = self.env['sale.order'].onchange_partner_id( + partner_id.id) + result.update(onchange_val['value']) + return result + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@job(default_channel='root.woo') +def sale_order_import_batch(session, model_name, backend_id, filters=None): + """ Prepare the import of Sale Order modified on Woo """ + env = get_environment(session, model_name, backend_id) + importer = env.get_connector_unit(SaleOrderBatchImporter) + importer.run(filters=filters) diff --git a/connector_woocommerce/related_action.py b/connector_woocommerce/related_action.py new file mode 100755 index 0000000..aaabb39 --- /dev/null +++ b/connector_woocommerce/related_action.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import functools +from openerp import exceptions, _ +from openerp.addons.connector import related_action +from .connector import get_environment +from .unit.backend_adapter import GenericAdapter +from .unit.binder import WooBinder + +unwrap_binding = functools.partial(related_action.unwrap_binding, + binder_class=WooBinder) + + +def link(session, job, backend_id_pos=2, woo_id_pos=3): + """ Open a Woo URL on the admin page to view/edit the record + related to the job. + """ + binding_model = job.args[0] + # shift one to the left because session is not in job.args + backend_id = job.args[backend_id_pos - 1] + woo_id = job.args[woo_id_pos - 1] + env = get_environment(session, binding_model, backend_id) + adapter = env.get_connector_unit(GenericAdapter) + try: + url = adapter.admin_url(woo_id) + except ValueError: + raise exceptions.Warning( + _('No admin URL configured on the backend or ' + 'no admin path is defined for this record.') + ) + + action = { + 'type': 'ir.actions.act_url', + 'target': 'new', + 'url': url, + } + return action diff --git a/connector_woocommerce/security/ir.model.access.csv b/connector_woocommerce/security/ir.model.access.csv new file mode 100755 index 0000000..7086502 --- /dev/null +++ b/connector_woocommerce/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_woo_sale_order_status,access_woo_sale_order_status,model_woo_sale_order_status,,1,0,0,0 +access_woo_sale_order_line,access_woo_sale_order_line,model_woo_sale_order_line,,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..0d5b58d7a36b84abfef13c2720c652afc39ddbbe GIT binary patch literal 12004 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. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from . import import_synchronizer +from . import backend_adapter +from . import mapper +from . import binder diff --git a/connector_woocommerce/unit/backend_adapter.py b/connector_woocommerce/unit/backend_adapter.py new file mode 100755 index 0000000..b6b9c95 --- /dev/null +++ b/connector_woocommerce/unit/backend_adapter.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import socket +import logging +import xmlrpclib +from woocommerce import API +from openerp.addons.connector.unit.backend_adapter import CRUDAdapter +from openerp.addons.connector.exception import (NetworkRetryableError, + RetryableJobError) +from datetime import datetime +_logger = logging.getLogger(__name__) + +recorder = {} + + +def call_to_key(method, arguments): + """ Used to 'freeze' the method and arguments of a call to WooCommerce + so they can be hashable; they will be stored in a dict. + + Used in both the recorder and the tests. + """ + def freeze(arg): + if isinstance(arg, dict): + items = dict((key, freeze(value)) for key, value + in arg.iteritems()) + return frozenset(items.iteritems()) + elif isinstance(arg, list): + return tuple([freeze(item) for item in arg]) + else: + return arg + + new_args = [] + for arg in arguments: + new_args.append(freeze(arg)) + return (method, tuple(new_args)) + + +def record(method, arguments, result): + """ Utility function which can be used to record test data + during synchronisations. Call it from WooCRUDAdapter._call + + Then ``output_recorder`` can be used to write the data recorded + to a file. + """ + recorder[call_to_key(method, arguments)] = result + + +def output_recorder(filename): + import pprint + with open(filename, 'w') as f: + pprint.pprint(recorder, f) + _logger.debug('recorder written to file %s', filename) + + +class WooLocation(object): + + def __init__(self, location, consumer_key, consumre_secret): + self._location = location + self.consumer_key = consumer_key + self.consumer_secret = consumre_secret + + @property + def location(self): + location = self._location + return location + + +class WooCRUDAdapter(CRUDAdapter): + + """ External Records Adapter for woo """ + + def __init__(self, connector_env): + """ + + :param connector_env: current environment (backend, session, ...) + :type connector_env: :class:`connector.connector.ConnectorEnvironment` + """ + super(WooCRUDAdapter, self).__init__(connector_env) + backend = self.backend_record + woo = WooLocation( + backend.location, + backend.consumer_key, + backend.consumer_secret) + self.woo = woo + + def search(self, filters=None): + """ Search records according to some criterias + and returns a list of ids """ + raise NotImplementedError + + def read(self, id, attributes=None): + """ Returns the information of a record """ + raise NotImplementedError + + def search_read(self, filters=None): + """ Search records according to some criterias + and returns their information""" + raise NotImplementedError + + def create(self, data): + """ Create a record on the external system """ + raise NotImplementedError + + def write(self, id, data): + """ Update records on the external system """ + raise NotImplementedError + + def delete(self, id): + """ Delete a record on the external system """ + raise NotImplementedError + + def _call(self, method, arguments): + try: + _logger.debug("Start calling Woocommerce api %s", method) + api = API(url=self.woo.location, + consumer_key=self.woo.consumer_key, + consumer_secret=self.woo.consumer_secret, + version='v2') + if api: + if isinstance(arguments, list): + while arguments and arguments[-1] is None: + arguments.pop() + start = datetime.now() + try: + if 'false' or 'true' or 'null'in api.get(method).content: + result = api.get(method).content.replace( + 'false', 'False') + result = result.replace('true', 'True') + result = result.replace('null', 'False') + result = eval(result) + else: + result = eval(api.get(method).content) + except: + _logger.error("api.call(%s, %s) failed", method, arguments) + raise + else: + _logger.debug("api.call(%s, %s) returned %s in %s seconds", + method, arguments, result, + (datetime.now() - start).seconds) + return result + except (socket.gaierror, socket.error, socket.timeout) as err: + raise NetworkRetryableError( + 'A network error caused the failure of the job: ' + '%s' % err) + except xmlrpclib.ProtocolError as err: + if err.errcode in [502, # Bad gateway + 503, # Service unavailable + 504]: # Gateway timeout + raise RetryableJobError( + 'A protocol error caused the failure of the job:\n' + 'URL: %s\n' + 'HTTP/HTTPS headers: %s\n' + 'Error code: %d\n' + 'Error message: %s\n' % + (err.url, err.headers, err.errcode, err.errmsg)) + else: + raise + + +class GenericAdapter(WooCRUDAdapter): + + _model_name = None + _woo_model = None + + def search(self, filters=None): + """ Search records according to some criterias + and returns a list of ids + + :rtype: list + """ + return self._call('%s.search' % self._woo_model, + [filters] if filters else [{}]) + + def read(self, id, attributes=None): + """ Returns the information of a record + + :rtype: dict + """ + arguments = [] + if attributes: + # Avoid to pass Null values in attributes. Workaround for + # is not installed, calling info() with None in attributes + # would return a wrong result (almost empty list of + # attributes). The right correction is to install the + # compatibility patch on WooCommerce. + arguments.append(attributes) + return self._call('%s/' % self._woo_model + str(id), []) + + def search_read(self, filters=None): + """ Search records according to some criterias + and returns their information""" + return self._call('%s.list' % self._woo_model, [filters]) + + def create(self, data): + """ Create a record on the external system """ + return self._call('%s.create' % self._woo_model, [data]) + + def write(self, id, data): + """ Update records on the external system """ + return self._call('%s.update' % self._woo_model, + [int(id), data]) + + def delete(self, id): + """ Delete a record on the external system """ + return self._call('%s.delete' % self._woo_model, [int(id)]) diff --git a/connector_woocommerce/unit/binder.py b/connector_woocommerce/unit/binder.py new file mode 100755 index 0000000..bf4cfc8 --- /dev/null +++ b/connector_woocommerce/unit/binder.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import openerp +from openerp.addons.connector.connector import Binder +from ..backend import woo + + +class WooBinder(Binder): + + """ Generic Binder for WooCommerce """ + + +@woo +class WooModelBinder(WooBinder): + + """ + Bindings are done directly on the binding model.woo.product.category + + Binding models are models called ``woo.{normal_model}``, + like ``woo.res.partner`` or ``woo.product.product``. + They are ``_inherits`` of the normal models and contains + the Woo ID, the ID of the Woo Backend and the additional + fields belonging to the Woo instance. + """ + _model_name = [ + 'woo.res.partner', + 'woo.product.category', + 'woo.product.product', + 'woo.sale.order', + 'woo.sale.order.line', + ] + + def to_openerp(self, external_id, unwrap=False, browse=False): + """ Give the OpenERP ID for an external ID + + :param external_id: external ID for which we want the OpenERP ID + :param unwrap: if True, returns the normal record (the one + inherits'ed), else return the binding record + :param browse: if True, returns a recordset + :return: a recordset of one record, depending on the value of unwrap, + or an empty recordset if no binding is found + :rtype: recordset + """ + bindings = self.model.with_context(active_test=False).search( + [('woo_id', '=', str(external_id)), + ('backend_id', '=', self.backend_record.id)] + ) + if not bindings: + return self.model.browse() if browse else None + assert len(bindings) == 1, "Several records found: %s" % (bindings,) + if unwrap: + return bindings.openerp_id if browse else bindings.openerp_id.id + else: + return bindings if browse else bindings.id + + def to_backend(self, record_id, wrap=False): + """ Give the external ID for an OpenERP ID + + :param record_id: OpenERP ID for which we want the external id + or a recordset with one record + :param wrap: if False, record_id is the ID of the binding, + if True, record_id is the ID of the normal record, the + method will search the corresponding binding and returns + the backend id of the binding + :return: backend identifier of the record + """ + record = self.model.browse() + if isinstance(record_id, openerp.models.BaseModel): + record_id.ensure_one() + record = record_id + record_id = record_id.id + if wrap: + binding = self.model.with_context(active_test=False).search( + [('openerp_id', '=', record_id), + ('backend_id', '=', self.backend_record.id), + ] + ) + if binding: + binding.ensure_one() + return binding.woo_id + else: + return None + if not record: + record = self.model.browse(record_id) + assert record + return record.woo_id + + def bind(self, external_id, binding_id): + """ Create the link between an external ID and an OpenERP ID and + update the last synchronization date. + + :param external_id: External ID to bind + :param binding_id: OpenERP ID to bind + :type binding_id: int + """ + # the external ID can be 0 on Woo! Prevent False values + # like False, None, or "", but not 0. + assert (external_id or external_id == 0) and binding_id, ( + "external_id or binding_id missing, " + "got: %s, %s" % (external_id, binding_id) + ) + # avoid to trigger the export when we modify the `woo_id` + now_fmt = openerp.fields.Datetime.now() + if not isinstance(binding_id, openerp.models.BaseModel): + binding_id = self.model.browse(binding_id) + binding_id.with_context(connector_no_export=True).write( + {'woo_id': str(external_id), + 'sync_date': now_fmt, + }) + + def unwrap_binding(self, binding_id, browse=False): + """ For a binding record, gives the normal record. + + Example: when called with a ``woo.product.product`` id, + it will return the corresponding ``product.product`` id. + + :param browse: when True, returns a browse_record instance + rather than an ID + """ + if isinstance(binding_id, openerp.models.BaseModel): + binding = binding_id + else: + binding = self.model.browse(binding_id) + + openerp_record = binding.openerp_id + if browse: + return openerp_record + return openerp_record.id + + def unwrap_model(self): + """ For a binding model, gives the name of the normal model. + + Example: when called on a binder for ``woo.product.product``, + it will return ``product.product``. + + This binder assumes that the normal model lays in ``openerp_id`` since + this is the field we use in the ``_inherits`` bindings. + """ + try: + column = self.model._fields['openerp_id'] + except KeyError: + raise ValueError('Cannot unwrap model %s, because it has ' + 'no openerp_id field' % self.model._name) + return column.comodel_name diff --git a/connector_woocommerce/unit/import_synchronizer.py b/connector_woocommerce/unit/import_synchronizer.py new file mode 100755 index 0000000..fb3535a --- /dev/null +++ b/connector_woocommerce/unit/import_synchronizer.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +import logging +from openerp import fields, _ +from openerp.addons.connector.queue.job import job, related_action +from openerp.addons.connector.unit.synchronizer import Importer +from openerp.addons.connector.exception import IDMissingInBackend +from ..connector import get_environment +from ..related_action import link +from datetime import datetime +_logger = logging.getLogger(__name__) + + +class WooImporter(Importer): + + """ Base importer for WooCommerce """ + + def __init__(self, connector_env): + """ + :param connector_env: current environment (backend, session, ...) + :type connector_env: :class:`connector.connector.ConnectorEnvironment` + """ + super(WooImporter, self).__init__(connector_env) + self.woo_id = None + self.woo_record = None + + def _get_woo_data(self): + """ Return the raw WooCommerce data for ``self.woo_id`` """ + return self.backend_adapter.read(self.woo_id) + + def _before_import(self): + """ Hook called before the import, when we have the WooCommerce + data""" + + def _is_uptodate(self, binding): + """Return True if the import should be skipped because + it is already up-to-date in OpenERP""" + WOO_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' + dt_fmt = WOO_DATETIME_FORMAT + assert self.woo_record + if not self.woo_record: + return # no update date on WooCommerce, always import it. + if not binding: + return # it does not exist so it should not be skipped + sync = binding.sync_date + if not sync: + return + from_string = fields.Datetime.from_string + sync_date = from_string(sync) + self.woo_record['updated_at'] = {} + self.woo_record['updated_at'] = {'to': datetime.now().strftime(dt_fmt)} + woo_date = from_string(self.woo_record['updated_at']['to']) + # if the last synchronization date is greater than the last + # update in woo, we skip the import. + # Important: at the beginning of the exporters flows, we have to + # check if the woo_date is more recent than the sync_date + # and if so, schedule a new import. If we don't do that, we'll + # miss changes done in WooCommerce + return woo_date < sync_date + + def _import_dependency(self, woo_id, binding_model, + importer_class=None, always=False): + """ Import a dependency. + + The importer class is a class or subclass of + :class:`WooImporter`. A specific class can be defined. + + :param woo_id: id of the related binding to import + :param binding_model: name of the binding model for the relation + :type binding_model: str | unicode + :param importer_cls: :class:`openerp.addons.connector.\ + connector.ConnectorUnit` + class or parent class to use for the export. + By default: WooImporter + :type importer_cls: :class:`openerp.addons.connector.\ + connector.MetaConnectorUnit` + :param always: if True, the record is updated even if it already + exists, note that it is still skipped if it has + not been modified on WooCommerce since the last + update. When False, it will import it only when + it does not yet exist. + :type always: boolean + """ + if not woo_id: + return + if importer_class is None: + importer_class = WooImporter + binder = self.binder_for(binding_model) + if always or binder.to_openerp(woo_id) is None: + importer = self.unit_for(importer_class, model=binding_model) + importer.run(woo_id) + + def _import_dependencies(self): + """ Import the dependencies for the record + + Import of dependencies can be done manually or by calling + :meth:`_import_dependency` for each dependency. + """ + return + + def _map_data(self): + """ Returns an instance of + :py:class:`~openerp.addons.connector.unit.mapper.MapRecord` + + """ + return self.mapper.map_record(self.woo_record) + + def _validate_data(self, data): + """ Check if the values to import are correct + + Pro-actively check before the ``_create`` or + ``_update`` if some fields are missing or invalid. + + Raise `InvalidDataError` + """ + return + + def _must_skip(self): + """ Hook called right after we read the data from the backend. + + If the method returns a message giving a reason for the + skipping, the import will be interrupted and the message + recorded in the job (if the import is called directly by the + job, not by dependencies). + + If it returns None, the import will continue normally. + + :returns: None | str | unicode + """ + return + + def _get_binding(self): + return self.binder.to_openerp(self.woo_id, browse=True) + + def _create_data(self, map_record, **kwargs): + return map_record.values(for_create=True, **kwargs) + + def _create(self, data): + """ Create the OpenERP record """ + # special check on data before import + self._validate_data(data) + model = self.model.with_context(connector_no_export=True) + model = str(model).split('()')[0] + binding = self.env[model].create(data) + _logger.debug('%d created from woo %s', binding, self.woo_id) + return binding + + def _update_data(self, map_record, **kwargs): + return map_record.values(**kwargs) + + def _update(self, binding, data): + """ Update an OpenERP record """ + # special check on data before import + self._validate_data(data) + binding.with_context(connector_no_export=True).write(data) + _logger.debug('%d updated from woo %s', binding, self.woo_id) + return + + def _after_import(self, binding): + """ Hook called at the end of the import """ + return + + def run(self, woo_id, force=False): + """ Run the synchronization + + :param woo_id: identifier of the record on WooCommerce + """ + self.woo_id = woo_id + try: + self.woo_record = self._get_woo_data() + except IDMissingInBackend: + return _('Record does no longer exist in WooCommerce') + + skip = self._must_skip() + if skip: + return skip + + binding = self._get_binding() + if not force and self._is_uptodate(binding): + return _('Already up-to-date.') + self._before_import() + + # import the missing linked resources + self._import_dependencies() + + map_record = self._map_data() + + if binding: + record = self._update_data(map_record) + self._update(binding, record) + else: + record = self._create_data(map_record) + binding = self._create(record) + self.binder.bind(self.woo_id, binding) + + self._after_import(binding) + + +WooImportSynchronizer = WooImporter + + +class BatchImporter(Importer): + + """ 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. + """ + + def run(self, filters=None): + """ Run the synchronization """ + record_ids = self.backend_adapter.search(filters) + for record_id in record_ids: + self._import_record(record_id) + + def _import_record(self, record_id): + """ Import a record directly or delay the import of the record. + + Method to implement in sub-classes. + """ + raise NotImplementedError + + +BatchImportSynchronizer = BatchImporter + + +class DirectBatchImporter(BatchImporter): + + """ Import the records directly, without delaying the jobs. """ + _model_name = None + + def _import_record(self, record_id): + """ Import the record directly """ + import_record(self.session, + self.model._name, + self.backend_record.id, + record_id) + + +DirectBatchImport = DirectBatchImporter + + +class DelayedBatchImporter(BatchImporter): + + """ Delay import of the records """ + _model_name = None + + def _import_record(self, record_id, **kwargs): + """ Delay the import of the records""" + import_record.delay(self.session, + self.model._name, + self.backend_record.id, + record_id, + **kwargs) + + +DelayedBatchImport = DelayedBatchImporter + + +@job(default_channel='root.woo') +@related_action(action=link) +def import_record(session, model_name, backend_id, woo_id, force=False): + """ Import a record from Woo """ + env = get_environment(session, model_name, backend_id) + importer = env.get_connector_unit(WooImporter) + importer.run(woo_id, force=force) diff --git a/connector_woocommerce/unit/mapper.py b/connector_woocommerce/unit/mapper.py new file mode 100755 index 0000000..1d148a7 --- /dev/null +++ b/connector_woocommerce/unit/mapper.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# +# Tech-Receptives Solutions Pvt. Ltd. +# Copyright (C) 2009-TODAY Tech-Receptives(). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + + +def normalize_datetime(field): + """Change a invalid date which comes from Woo, if + no real date is set to null for correct import to + OpenERP""" + + def modifier(self, record, to_attr): + if record[field] == '0000-00-00 00:00:00': + return None + return record[field] + return modifier diff --git a/connector_woocommerce/views/backend_view.xml b/connector_woocommerce/views/backend_view.xml new file mode 100755 index 0000000..037d4f2 --- /dev/null +++ b/connector_woocommerce/views/backend_view.xml @@ -0,0 +1,95 @@ + + + + + wc.backend.tree + wc.backend + tree + + + + + + + + + + wc.backend.form + wc.backend + form + + +
+
+
+ +