' + binding.note + '
', # field is HTML, note is expected to be escaped + 'user_id': user.id, + 'res_id': binding.odoo_id.id, + 'res_model_id': self.env.ref('sale.model_sale_order').id, + }) + + def _create(self, data): + binding = super(SaleOrderImporter, self)._create(data) + # Without this, it won't map taxes with the fiscal position. + if binding.fiscal_position_id: + binding.odoo_id._compute_tax_id() + + self._order_comment_review(binding) + + return binding + + def _import_dependencies(self): + record = self.opencart_record + self._import_addresses() + products_need_setup = [] + for product in record.get('products', []): + if 'product_id' in product and product['product_id']: + needs_product_setup = self._import_dependency(product['product_id'], 'opencart.product.template') + if needs_product_setup: + products_need_setup.append(product['product_id']) + + if products_need_setup and self.backend_record.so_require_product_setup: + # There are products that were either just imported, or + raise RetryableJobError('Products need setup. OpenCart Product IDs:' + str(products_need_setup), seconds=3600) + + +class SaleOrderLineImportMapper(Component): + + _name = 'opencart.sale.order.line.mapper' + _inherit = 'opencart.import.mapper' + _apply_on = 'opencart.sale.order.line' + + direct = [('quantity', 'product_uom_qty'), + ('price', 'price_unit'), + ('order_product_id', 'external_id'), + ] + + @mapping + def name(self, record): + return {'name': unescape(record['name'])} + + @mapping + def product_id(self, record): + product_id = record['product_id'] + binder = self.binder_for('opencart.product.template') + # do not unwrap, because it would be a product.template, but I need a specific variant + # connector bindings are found with `active_test=False` but that also means computed fields + # like `product.template.product_variant_id` could find different products because of archived variants + opencart_product_template = binder.to_internal(product_id, unwrap=False).with_context(active_test=True) + product = opencart_product_template.opencart_sale_get_combination(record.get('option')) + return {'product_id': product.id, 'product_uom': product.uom_id.id} diff --git a/connector_opencart/models/stock_picking/__init__.py b/connector_opencart/models/stock_picking/__init__.py new file mode 100644 index 00000000..2db3f18c --- /dev/null +++ b/connector_opencart/models/stock_picking/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import exporter diff --git a/connector_opencart/models/stock_picking/common.py b/connector_opencart/models/stock_picking/common.py new file mode 100644 index 00000000..da90a1d8 --- /dev/null +++ b/connector_opencart/models/stock_picking/common.py @@ -0,0 +1,88 @@ +# © 2019-2021 Hibou Corp. + +from odoo import api, models, fields, _ +from odoo.addons.component.core import Component +from odoo.addons.queue_job.exception import RetryableJobError + + +class OpencartStockPicking(models.Model): + _name = 'opencart.stock.picking' + _inherit = 'opencart.binding' + _inherits = {'stock.picking': 'odoo_id'} + _description = 'Opencart Delivery Order' + + odoo_id = fields.Many2one(comodel_name='stock.picking', + string='Stock Picking', + required=True, + ondelete='cascade') + opencart_order_id = fields.Many2one(comodel_name='opencart.sale.order', + string='Opencart Sale Order', + ondelete='set null') + + def export_picking_done(self): + """ Export a complete or partial delivery order. """ + self.ensure_one() + with self.backend_id.work_on(self._name) as work: + exporter = work.component(usage='record.exporter') + return exporter.run(self) + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + opencart_bind_ids = fields.One2many( + comodel_name='opencart.stock.picking', + inverse_name='odoo_id', + string="Opencart Bindings", + ) + + +class StockPickingAdapter(Component): + _name = 'opencart.stock.picking.adapter' + _inherit = 'opencart.adapter' + _apply_on = 'opencart.stock.picking' + + def create(self, id, tracking): + api_instance = self.api_instance + tracking_comment = _('Order shipped with tracking number: %s') % (tracking, ) + result = api_instance.orders.ship(id, tracking, tracking_comment) + if 'success' in result: + return result['success'] + raise RetryableJobError('Shipping Order %s did not return an order response. (tracking: %s) %s' % ( + str(id), str(tracking), str(result))) + + +class OpencartBindingStockPickingListener(Component): + _name = 'opencart.binding.stock.picking.listener' + _inherit = 'base.event.listener' + _apply_on = ['opencart.stock.picking'] + + def on_record_create(self, record, fields=None): + record.with_delay().export_picking_done() + + +class OpencartStockPickingListener(Component): + _name = 'opencart.stock.picking.listener' + _inherit = 'base.event.listener' + _apply_on = ['stock.picking'] + + def on_picking_dropship_done(self, record, picking_method): + return self.on_picking_out_done(record, picking_method) + + def on_picking_out_done(self, record, picking_method): + """ + Create a ``opencart.stock.picking`` record. This record will then + be exported to Opencart. + + :param picking_method: picking_method, can be 'complete' or 'partial' + :type picking_method: str + """ + sale = record.sale_id + if not sale: + return + for opencart_sale in sale.opencart_bind_ids: + self.env['opencart.stock.picking'].create({ + 'backend_id': opencart_sale.backend_id.id, + 'odoo_id': record.id, + 'opencart_order_id': opencart_sale.id, + }) diff --git a/connector_opencart/models/stock_picking/exporter.py b/connector_opencart/models/stock_picking/exporter.py new file mode 100644 index 00000000..36c391fd --- /dev/null +++ b/connector_opencart/models/stock_picking/exporter.py @@ -0,0 +1,35 @@ +# © 2019-2021 Hibou Corp. + +from odoo.addons.component.core import Component +from odoo.addons.queue_job.exception import NothingToDoJob + + +class OpencartPickingExporter(Component): + _name = 'opencart.stock.picking.exporter' + _inherit = 'opencart.exporter' + _apply_on = ['opencart.stock.picking'] + + def _get_id(self, binding): + sale_binder = self.binder_for('opencart.sale.order') + opencart_sale_id = sale_binder.to_external(binding.opencart_order_id) + return opencart_sale_id + + def _get_tracking(self, binding): + return binding.carrier_tracking_ref or '' + + def run(self, binding): + """ + Export the picking to Opencart + :param binding: opencart.stock.picking + :return: + """ + if binding.external_id: + return 'Already exported' + + tracking = self._get_tracking(binding) + if not tracking: + raise NothingToDoJob('Cancelled: the delivery order does not contain tracking.') + id = self._get_id(binding) + _ = self.backend_adapter.create(id, tracking) + # Cannot bind because shipments do not have ID's in Opencart + #self.binder.bind(external_id, binding) diff --git a/connector_opencart/security/ir.model.access.csv b/connector_opencart/security/ir.model.access.csv new file mode 100644 index 00000000..ccbfcb99 --- /dev/null +++ b/connector_opencart/security/ir.model.access.csv @@ -0,0 +1,15 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_opencart_backend","opencart_backend connector manager","model_opencart_backend","connector.group_connector_manager",1,1,1,1 +"access_opencart_store","opencart_store connector manager","model_opencart_store","connector.group_connector_manager",1,1,1,1 +"access_opencart_binding","opencart_binding connector manager","model_opencart_binding","connector.group_connector_manager",1,1,1,1 +"access_opencart_sale_order","opencart_sale_order connector manager","model_opencart_sale_order","connector.group_connector_manager",1,1,1,1 +"access_opencart_sale_order_line","opencart_sale_order_line connector manager","model_opencart_sale_order_line","connector.group_connector_manager",1,1,1,1 +"access_opencart_product_template","opencart_product_template connector manager","model_opencart_product_template","connector.group_connector_manager",1,1,1,1 +"access_opencart_product_template_attribute_value","opencart_product_template_attribute_value connector manager","model_opencart_product_template_attribute_value","connector.group_connector_manager",1,1,1,1 +"access_opencart_stock_picking","opencart_stock_picking connector manager","model_opencart_stock_picking","connector.group_connector_manager",1,1,1,1 +"access_opencart_sale_order_sale_salesman","opencart_sale_order","model_opencart_sale_order","sales_team.group_sale_salesman",1,0,0,0 +"access_opencart_sale_order_sale_manager","opencart_sale_order","model_opencart_sale_order","sales_team.group_sale_manager",1,1,1,1 +"access_opencart_sale_order_stock_user","opencart_sale_order warehouse user","model_opencart_sale_order","stock.group_stock_user",1,0,0,0 +"access_opencart_backend_user","opencart_backend user","model_opencart_backend","sales_team.group_sale_salesman",1,0,0,0 +"access_opencart_stock_picking_user","opencart_stock_picking user","model_opencart_stock_picking","stock.group_stock_user",1,1,1,0 +"access_opencart_product_template_user","opencart_product_template user","model_opencart_product_template","base.group_user",1,0,0,0 diff --git a/connector_opencart/views/delivery_views.xml b/connector_opencart/views/delivery_views.xml new file mode 100644 index 00000000..40d5bd26 --- /dev/null +++ b/connector_opencart/views/delivery_views.xml @@ -0,0 +1,19 @@ + +