diff --git a/connector_opencart/__manifest__.py b/connector_opencart/__manifest__.py index ce265249..ddd2c9bc 100644 --- a/connector_opencart/__manifest__.py +++ b/connector_opencart/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Opencart Connector', - 'version': '12.0.1.0.0', + 'version': '12.0.1.1.0', 'category': 'Connector', 'depends': [ 'account', @@ -17,9 +17,12 @@ 'license': 'AGPL-3', 'website': 'https://hibou.io', 'data': [ + 'data/connector_opencart_data.xml', 'security/ir.model.access.csv', 'views/delivery_views.xml', 'views/opencart_backend_views.xml', + 'views/opencart_product_views.xml', + 'views/product_views.xml', ], 'installable': True, 'application': False, diff --git a/connector_opencart/components/api/opencart.py b/connector_opencart/components/api/opencart.py index cd52828f..f0dfe367 100644 --- a/connector_opencart/components/api/opencart.py +++ b/connector_opencart/components/api/opencart.py @@ -5,6 +5,9 @@ import requests from urllib.parse import urlencode from json import loads, dumps +import logging +_logger = logging.getLogger(__name__) + class Opencart: @@ -22,6 +25,10 @@ class Opencart: def stores(self): return Stores(connection=self) + @property + def products(self): + return Products(connection=self) + def get_headers(self, url, method): headers = {} if method in ('POST', 'PUT', ): @@ -33,11 +40,19 @@ class Opencart: if params: encoded_url += '?%s' % urlencode(params) headers = self.get_headers(encoded_url, method) - + _logger.debug('send_request method: %s url: %s headers: %s params: %s body: %s' % ( + method, + url, + headers, + params, + body + )) if method == 'GET': - return loads(self.session.get(url, params=params, headers=headers).text) + result_text = self.session.get(url, params=params, headers=headers).text elif method == 'PUT' or method == 'POST': - return loads(self.session.put(url, data=body, headers=headers).text) + result_text = self.session.put(url, data=body, headers=headers).text + _logger.debug('raw_text: ' + str(result_text)) + return loads(result_text) class Resource: @@ -138,3 +153,14 @@ class Stores(Resource): def get(self, id): url = self.url + ('/%s' % id) return self.connection.send_request(method='GET', url=url) + + +class Products(Resource): + """ + Retrieves Product details + """ + path = 'products' + + def get(self, id): + url = self.url + ('/%s' % id) + return self.connection.send_request(method='GET', url=url) diff --git a/connector_opencart/components/binder.py b/connector_opencart/components/binder.py index d8bbaebd..b2525fea 100644 --- a/connector_opencart/components/binder.py +++ b/connector_opencart/components/binder.py @@ -20,4 +20,6 @@ class OpencartModelBinder(Component): 'opencart.sale.order', 'opencart.sale.order.line', 'opencart.stock.picking', + 'opencart.product.template', + 'opencart.product.template.attribute.value', ] diff --git a/connector_opencart/components/importer.py b/connector_opencart/components/importer.py index a2c7f973..6a1ef5db 100644 --- a/connector_opencart/components/importer.py +++ b/connector_opencart/components/importer.py @@ -88,7 +88,8 @@ class OpencartImporter(AbstractComponent): if not external_id: return binder = self.binder_for(binding_model) - if always or not binder.to_internal(external_id): + record = binder.to_internal(external_id) + if always or not record: if importer is None: importer = self.component(usage='record.importer', model_name=binding_model) @@ -99,6 +100,13 @@ class OpencartImporter(AbstractComponent): 'Dependency import of %s(%s) has been ignored.', binding_model._name, external_id ) + return True + if binding_model == 'opencart.product.template' and record.backend_id.so_require_product_setup: + # Though this is not the "right" place to do this, + # we need to return True if there is a checkpoint for a product. + if record.backend_id.find_checkpoint(record): + return True + return False def _import_dependencies(self): """ Import the dependencies for the record diff --git a/connector_opencart/data/connector_opencart_data.xml b/connector_opencart/data/connector_opencart_data.xml index fef62b5b..b575ec5d 100644 --- a/connector_opencart/data/connector_opencart_data.xml +++ b/connector_opencart/data/connector_opencart_data.xml @@ -4,13 +4,13 @@ Opencart - Import Sales Orders - + code 1 - days + hours -1 - + model._scheduler_import_sale_orders() @@ -27,26 +27,7 @@ Check your taxes and fiscal positions configuration and correct them if necessar 30 sale.order sale - if sale.opencart_bind_ids and abs(sale.amount_total - sale.opencart_bind_ids[0].total_amount) >= 0.01: - failed = True - - - - - Total Tax Amount differs from Opencart - The tax amount computed in Odoo doesn't match with the tax amount in Opencart. - -Cause: -The taxes are probably different between Odoo and Opencart. A fiscal position could have changed the final price. - -Resolution: -Check your taxes and fiscal positions configuration and correct them if necessary. - 30 - sale.order - sale - # By default, a cent of difference for the tax amount is allowed, feel free to customise it in your own module -if sale.opencart_bind_ids and abs(sale.amount_tax - sale.opencart_bind_ids[0].total_amount_tax) > 0.01: - failed = True + failed = sale.opencart_bind_ids and abs(sale.amount_total - sale.opencart_bind_ids[0].total_amount) >= 0.01 diff --git a/connector_opencart/models/__init__.py b/connector_opencart/models/__init__.py index 4ed0f156..a7f55577 100644 --- a/connector_opencart/models/__init__.py +++ b/connector_opencart/models/__init__.py @@ -1,4 +1,5 @@ from . import delivery from . import opencart +from . import product from . import sale_order from . import stock_picking diff --git a/connector_opencart/models/opencart/backend.py b/connector_opencart/models/opencart/backend.py index f3b344ce..42cd62ad 100644 --- a/connector_opencart/models/opencart/backend.py +++ b/connector_opencart/models/opencart/backend.py @@ -2,12 +2,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta from logging import getLogger from contextlib import contextmanager from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.addons.connector.models.checkpoint import add_checkpoint from ...components.api.opencart import Opencart _logger = getLogger(__name__) @@ -62,6 +62,8 @@ class OpencartBackend(models.Model): "in Odoo.", ) # payment_mode_id = fields.Many2one(comodel_name='account.payment.mode', string="Payment Mode") + coupon_product_id = fields.Many2one(comodel_name='product.product', string='Coupon Product', + help='Product to represent coupon discounts.') # New Product fields. product_categ_id = fields.Many2one(comodel_name='product.category', string='Product Category', @@ -71,6 +73,9 @@ class OpencartBackend(models.Model): string='Import sale orders after id', ) + so_require_product_setup = fields.Boolean(string='SO Require Product Setup', + help='Prevents SO from being confirmed (failed queue job), if one or more products has an open checkpoint.') + @contextmanager @api.multi def work_on(self, model_name, **kwargs): @@ -80,6 +85,27 @@ class OpencartBackend(models.Model): with _super.work_on(model_name, opencart_api=opencart_api, **kwargs) as work: yield work + @api.multi + def add_checkpoint(self, record): + self.ensure_one() + record.ensure_one() + return add_checkpoint(self.env, record._name, record.id, + self._name, self.id) + + @api.multi + def find_checkpoint(self, record): + self.ensure_one() + record.ensure_one() + checkpoint_model = self.env['connector.checkpoint'] + model_model = self.env['ir.model'] + model = model_model.search([('model', '=', record._name)], limit=1) + return checkpoint_model.search([ + ('backend_id', '=', '%s,%s' % (self._name, self.id)), + ('model_id', '=', model.id), + ('record_id', '=', record.id), + ('state', '=', 'need_review'), + ], limit=1) + @api.multi def synchronize_metadata(self): try: diff --git a/connector_opencart/models/opencart/store.py b/connector_opencart/models/opencart/store.py index 31e71e39..4444b16f 100644 --- a/connector_opencart/models/opencart/store.py +++ b/connector_opencart/models/opencart/store.py @@ -50,6 +50,8 @@ class OpencartStore(models.Model): "order 36071 in Opencart, will be named 'OC-36071' " "in Odoo. (overridden from backend)", ) + coupon_product_id = fields.Many2one(comodel_name='product.product', string='Coupon Product', + help='Product to represent coupon discounts.') class OpencartStoreAdapter(Component): diff --git a/connector_opencart/models/product/__init__.py b/connector_opencart/models/product/__init__.py new file mode 100644 index 00000000..79ab5dc6 --- /dev/null +++ b/connector_opencart/models/product/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import importer diff --git a/connector_opencart/models/product/common.py b/connector_opencart/models/product/common.py new file mode 100644 index 00000000..4d169803 --- /dev/null +++ b/connector_opencart/models/product/common.py @@ -0,0 +1,81 @@ +from odoo import api, fields, models +from odoo.addons.queue_job.exception import NothingToDoJob, RetryableJobError +from odoo.addons.component.core import Component + + +class OpencartProductTemplate(models.Model): + _name = 'opencart.product.template' + _inherit = 'opencart.binding' + _inherits = {'product.template': 'odoo_id'} + _description = 'Opencart Product' + + odoo_id = fields.Many2one('product.template', + string='Product', + required=True, + ondelete='cascade') # cascade so that you can delete an Odoo product that was created by connector + opencart_attribute_value_ids = fields.One2many('opencart.product.template.attribute.value', + 'opencart_product_tmpl_id', + string='Opencart Product Attribute Values') + + def opencart_sale_get_combination(self, options, reentry=False): + selected_attribute_values = self.env['product.template.attribute.value'] + for option in options: + product_option_value_id = str(option['product_option_value_id']) + opencart_attribute_value = self.opencart_attribute_value_ids.filtered(lambda v: v.external_id == product_option_value_id) + if not opencart_attribute_value: + if reentry: + # we have already triggered an import. + raise Exception('Order Product has option (%s) "%s" that does not exist on the product.' % (product_option_value_id, option.get('name', ''))) + # need to re-import product. + try: + self.import_record(self.backend_id, self.external_id, force=True) + return self.opencart_sale_get_combination(options, reentry=True) + except NothingToDoJob: + if reentry: + raise RetryableJobError('Product imported, but selected option is not available.') + if not opencart_attribute_value.odoo_id: + raise RetryableJobError('Order Product (%s) has option (%s) "%s" that is not mapped to an Odoo Attribute Value.' % (self, opencart_attribute_value.external_id, opencart_attribute_value.opencart_name)) + selected_attribute_values += opencart_attribute_value.odoo_id + # Now that we know what options are selected, we can load a variant with those options + product = self.odoo_id._create_product_variant(selected_attribute_values, log_warning=True) + if not product: + raise Exception('No product can be created for selected attribute values, check logs. ' + str(selected_attribute_values)) + return product + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + opencart_sku = fields.Char('Opencart SKU') + opencart_bind_ids = fields.One2many('opencart.product.template', 'odoo_id', string='Opencart Bindings') + + +class OpencartProductTemplateAdapter(Component): + _name = 'opencart.product.template.adapter' + _inherit = 'opencart.adapter' + _apply_on = 'opencart.product.template' + + def read(self, id): + api_instance = self.api_instance + record = api_instance.products.get(id) + if 'data' in record and record['data']: + return record['data'] + raise RetryableJobError('Product "' + str(id) + '" did not return an product response. ' + str(record)) + + +# Product Attribute Value, cannot "inherits" the odoo_id as then it cannot be empty +class OpencartProductTemplateAttributeValue(models.Model): + _name = 'opencart.product.template.attribute.value' + _inherit = 'opencart.binding' + _description = 'Opencart Product Attribute Value' + + odoo_id = fields.Many2one('product.template.attribute.value', + string='Product Attribute Value', + required=False, + ondelete='cascade') + opencart_name = fields.Char(string='Opencart Name', help='For matching purposes.') + opencart_product_tmpl_id = fields.Many2one('opencart.product.template', + string='Opencart Product', + required=True, + ondelete='cascade') + product_tmpl_id = fields.Many2one(related='opencart_product_tmpl_id.odoo_id') diff --git a/connector_opencart/models/product/importer.py b/connector_opencart/models/product/importer.py new file mode 100644 index 00000000..c7988323 --- /dev/null +++ b/connector_opencart/models/product/importer.py @@ -0,0 +1,91 @@ +from html import unescape +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create + + +class ProductImportMapper(Component): + _name = 'opencart.product.template.import.mapper' + _inherit = 'opencart.import.mapper' + _apply_on = ['opencart.product.template'] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + @mapping + def name(self, record): + name = record.get('product_description', [{}])[0].get('name', record.get('id')) + return {'name': unescape(name)} + + # TODO more fields like pricing.... + + @mapping + def product_type(self, record): + return {'type': 'product' if record.get('shipping') else 'service'} + + @mapping + def opencart_sku(self, record): + sku = str(record.get('model') or record.get('sku') or '').strip() + return {'opencart_sku': sku} + + @only_create + @mapping + def existing_product(self, record): + product_template = self.env['product.template'] + template = product_template.browse() + + if record.get('model'): + model = str(record.get('model') or '').strip() + # Try to match our own field + template = product_template.search([('opencart_sku', '=', model)], limit=1) + if not template: + # Try to match the default_code + template = product_template.search([('default_code', '=', model)], limit=1) + if not template and record.get('sku'): + sku = str(record.get('sku') or '').strip() + template = product_template.search([('opencart_sku', '=', sku)], limit=1) + if not template: + template = product_template.search([('default_code', '=', sku)], limit=1) + if not template and record.get('name'): + name = record.get('product_description', [{}])[0].get('name') + if name: + template = product_template.search([('name', '=', unescape(name))], limit=1) + return {'odoo_id': template.id} + + +class ProductImporter(Component): + _name = 'opencart.product.template.importer' + _inherit = 'opencart.importer' + _apply_on = ['opencart.product.template'] + + def _create(self, data): + binding = super(ProductImporter, self)._create(data) + self.backend_record.add_checkpoint(binding) + return binding + + def _after_import(self, binding): + self._sync_options(binding) + + def _sync_options(self, binding): + existing_option_values = binding.opencart_attribute_value_ids + mapped_option_values = binding.opencart_attribute_value_ids.browse() + record = self.opencart_record + backend = self.backend_record + for option in record.get('options', []): + for record_option_value in option.get('option_value', []): + option_value = existing_option_values.filtered(lambda v: v.external_id == str(record_option_value['product_option_value_id'])) + name = unescape(record_option_value.get('name', '')) + if not option_value: + option_value = existing_option_values.create({ + 'backend_id': backend.id, + 'external_id': record_option_value['product_option_value_id'], + 'opencart_name': name, + 'opencart_product_tmpl_id': binding.id, + }) + # Keep options consistent with Opencart by renaming them + if option_value.opencart_name != name: + option_value.opencart_name = name + mapped_option_values += option_value + + to_unlink = existing_option_values - mapped_option_values + to_unlink.unlink() diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index 42813627..79e19595 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -2,11 +2,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from copy import deepcopy, copy +from html import unescape from odoo import fields, _ from odoo.addons.component.core import Component from odoo.addons.connector.components.mapper import mapping from odoo.exceptions import ValidationError +from odoo.addons.queue_job.exception import RetryableJobError class SaleOrderBatchImporter(Component): @@ -52,31 +54,55 @@ class SaleOrderImportMapper(Component): direct = [('order_id', 'external_id'), ('store_id', 'store_id'), - # ('customerOrderId', 'customer_order_id'), ] children = [('products', 'opencart_order_line_ids', 'opencart.sale.order.line'), ] - # def _map_child(self, map_record, from_attr, to_attr, model_name): - # return super(SaleOrderImportMapper, self)._map_child(map_record, from_attr, to_attr, model_name) + def _add_coupon_lines(self, map_record, values): + # Data from API + # 'coupons': [{'amount': '7.68', 'code': '1111'}], + record = map_record.source + + coupons = record.get('coupons') + if not coupons: + return values + + coupon_product = self.options.store.coupon_product_id or self.backend_record.coupon_product_id + if not coupon_product: + coupon_product = self.env.ref('connector_ecommerce.product_product_discount', raise_if_not_found=False) + + if not coupon_product: + raise ValueError('Coupon %s on order requires coupon product in configuration.' % (coupons, )) + for coupon in coupons: + line_builder = self.component(usage='order.line.builder') + line_builder.price_unit = -float(coupon.get('amount', 0.0)) + line_builder.product = coupon_product + # `order.line.builder` does not allow naming. + line_values = line_builder.get_line() + code = coupon.get('code') + if code: + line_values['name'] = '%s Code: %s' % (coupon_product.name, code) + values['order_line'].append((0, 0, line_values)) + return values def _add_shipping_line(self, map_record, values): record = map_record.source line_builder = self.component(usage='order.line.builder.shipping') - line_builder.price_unit = 0.0 + line_builder.price_unit = record.get('shipping_exclude_tax', 0.0) if values.get('carrier_id'): carrier = self.env['delivery.carrier'].browse(values['carrier_id']) line_builder.product = carrier.product_id + line = (0, 0, line_builder.get_line()) + values['order_line'].append(line) - line = (0, 0, line_builder.get_line()) - values['order_line'].append(line) return values def finalize(self, map_record, values): values.setdefault('order_line', []) + self._add_coupon_lines(map_record, values) self._add_shipping_line(map_record, values) values.update({ 'partner_id': self.options.partner_id, @@ -120,9 +146,8 @@ class SaleOrderImportMapper(Component): [('name', '=', record_method)], limit=1, ) - assert method, ("method %s should exist because the import fails " - "in SaleOrderImporter._before_import when it is " - " missing" % record_method) + if not method: + raise ValueError('Payment Mode named "%s", cannot be found.' % (record_method, )) return {'payment_mode_id': method.id} @mapping @@ -138,8 +163,11 @@ class SaleOrderImportMapper(Component): return {'warehouse_id': warehouse.id} @mapping - def shipping_method(self, record): - method = record['shipping_method'] or '' + def shipping_code(self, record): + method = record.get('shipping_code') or record.get('shipping_method') + if not method: + return {'carrier_id': False} + carrier_domain = [('opencart_code', '=', method.strip())] company = self.options.store.company_id or self.backend_record.company_id if company: @@ -148,8 +176,8 @@ class SaleOrderImportMapper(Component): ] carrier = self.env['delivery.carrier'].search(carrier_domain, limit=1) if not carrier: - raise ValueError('Delivery Carrier for methodCode "%s", cannot be found.' % (method, )) - return {'carrier_id': carrier.id, 'shipping_method_code': method} + raise ValueError('Delivery Carrier for method Code "%s", cannot be found.' % (method, )) + return {'carrier_id': carrier.id} @mapping def company_id(self, record): @@ -186,6 +214,9 @@ class SaleOrderImporter(Component): def _partner_matches(self, partner, values): for key, value in values.items(): + if key in ('active', 'parent_id', 'type'): + continue + if key == 'state_id': if value != partner.state_id.id: return False @@ -197,7 +228,7 @@ class SaleOrderImporter(Component): return True def _make_partner_name(self, firstname, lastname): - name = (str(firstname) + ' ' + str(lastname)).strip() + name = (str(firstname or '').strip() + ' ' + str(lastname or '').strip()).strip() if not name: return 'Undefined' return name @@ -233,24 +264,31 @@ class SaleOrderImporter(Component): ], limit=1) return { - 'email': email, - 'name': name, - 'phone': phone, - 'street': street, - 'street2': street2, - 'zip': zip_, - 'city': city, + 'email': email.strip(), + 'name': name.strip(), + 'phone': phone.strip(), + 'street': street.strip(), + 'street2': street2.strip(), + 'zip': zip_.strip(), + 'city': city.strip(), 'state_id': state.id, 'country_id': country.id, } def _import_addresses(self): - record = self.opencart_record - partner_values = self._get_partner_values() - partner = self.env['res.partner'].search([ + partners = self.env['res.partner'].search([ ('email', '=', partner_values['email']), - ], limit=1) + '|', ('active', '=', False), ('active', '=', True), + ], order='active DESC, id ASC') + + partner = None + for possible in partners: + if self._partner_matches(possible, partner_values): + partner = possible + break + if not partner and partners: + partner = partners[0] if not partner: # create partner. @@ -258,18 +296,25 @@ class SaleOrderImporter(Component): if not self._partner_matches(partner, partner_values): partner_values['parent_id'] = partner.id - partner_values['active'] = False - shipping_partner = self._create_partner(copy(partner_values)) + shipping_values = copy(partner_values) + shipping_values['type'] = 'delivery' + shipping_partner = self._create_partner(shipping_values) else: shipping_partner = partner invoice_values = self._get_partner_values(info_string='payment_') + invoice_values['type'] = 'invoice' if (not self._partner_matches(partner, invoice_values) and not self._partner_matches(shipping_partner, invoice_values)): - partner_values['parent_id'] = partner.id - partner_values['active'] = False - invoice_partner = self._create_partner(copy(invoice_values)) + # Try to find existing invoice address.... + for possible in partners: + if self._partner_matches(possible, invoice_values): + invoice_partner = possible + break + else: + invoice_values['parent_id'] = partner.id + invoice_partner = self._create_partner(copy(invoice_values)) elif self._partner_matches(partner, invoice_values): invoice_partner = partner elif self._partner_matches(shipping_partner, invoice_values): @@ -317,7 +362,18 @@ class SaleOrderImporter(Component): 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): @@ -328,33 +384,21 @@ class SaleOrderLineImportMapper(Component): direct = [('quantity', 'product_uom_qty'), ('price', 'price_unit'), - ('name', 'name'), ('order_product_id', 'external_id'), ] - def _finalize_product_values(self, record, values): - # This would be a good place to create a vendor or add a route... - return values - - def _product_values(self, record): - reference = record['model'] - values = { - 'default_code': reference, - 'name': record.get('name', reference), - 'type': 'product', - 'list_price': record.get('price', 0.0), - 'categ_id': self.backend_record.product_categ_id.id, - } - return self._finalize_product_values(record, values) + @mapping + def name(self, record): + return {'name': unescape(record['name'])} @mapping def product_id(self, record): - reference = record['model'] - product = self.env['product.product'].search([ - ('default_code', '=', reference) - ], limit=1) - - if not product: - # we could use a record like (0, 0, values) - product = self.env['product.product'].create(self._product_values(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 + opencart_product_template = binder.to_internal(product_id, unwrap=False) + if record.get('option'): + product = opencart_product_template.opencart_sale_get_combination(record.get('option')) + else: + product = opencart_product_template.odoo_id.product_variant_id return {'product_id': product.id, 'product_uom': product.uom_id.id} diff --git a/connector_opencart/security/ir.model.access.csv b/connector_opencart/security/ir.model.access.csv index 44dcff29..2a8d2dfd 100644 --- a/connector_opencart/security/ir.model.access.csv +++ b/connector_opencart/security/ir.model.access.csv @@ -4,6 +4,8 @@ "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 diff --git a/connector_opencart/views/opencart_backend_views.xml b/connector_opencart/views/opencart_backend_views.xml index 360b9315..d60fda7b 100644 --- a/connector_opencart/views/opencart_backend_views.xml +++ b/connector_opencart/views/opencart_backend_views.xml @@ -37,6 +37,8 @@ + + @@ -131,6 +133,7 @@ + diff --git a/connector_opencart/views/opencart_product_views.xml b/connector_opencart/views/opencart_product_views.xml new file mode 100644 index 00000000..4cec551f --- /dev/null +++ b/connector_opencart/views/opencart_product_views.xml @@ -0,0 +1,56 @@ + + + + + opencart.product.template.form + opencart.product.template + +
+
+ + + + + + + + + + + + + + + + + + + + opencart.product.template.tree + opencart.product.template + + + + + + + + + + + + + Opencart Products + opencart.product.template + form + tree,form + + + + + + \ No newline at end of file diff --git a/connector_opencart/views/product_views.xml b/connector_opencart/views/product_views.xml new file mode 100644 index 00000000..3ccc3955 --- /dev/null +++ b/connector_opencart/views/product_views.xml @@ -0,0 +1,15 @@ + + + + + product.template.product.form.inherit + product.template + + + + + + + + + \ No newline at end of file