# © 2021 Hibou Corp. from base64 import b64encode from datetime import timedelta from odoo import api, fields, models from odoo.exceptions import UserError from odoo.addons.component.core import Component PRODUCT_SKU_WITH_WAREHOUSE = '%s-%s' class AmazonProductProduct(models.Model): _name = 'amazon.product.product' _inherit = 'amazon.binding' _inherits = {'product.product': 'odoo_id'} _description = 'Amazon Product Listing' _rec_name = 'external_id' odoo_id = fields.Many2one('product.product', string='Product', required=True, ondelete='cascade') asin = fields.Char(string='ASIN') state = fields.Selection([ ('draft', 'Draft'), ('sent', 'Submitted'), ], default='draft') warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', ondelete='set null') backend_warehouse_ids = fields.Many2many(related='backend_id.warehouse_ids') backend_fba_warehouse_ids = fields.Many2many(related='backend_id.fba_warehouse_ids') date_product_sent = fields.Datetime(string='Last Product Update') date_price_sent = fields.Datetime(string='Last Price Update') date_inventory_sent = fields.Datetime(string='Last Inventory Update') buffer_qty = fields.Integer(string='Buffer Quantity', help='Stock to hold back from Amazon for listings. (-1 means use the backend default)', default=-1) @api.onchange('odoo_id', 'warehouse_id', 'default_code') def _onchange_suggest_external_id(self): with_code_and_warehouse = self.filtered(lambda p: p.default_code and p.warehouse_id) with_code = (self - with_code_and_warehouse).filtered('default_code') other = (self - with_code_and_warehouse - with_code) for product in with_code_and_warehouse: product.external_id = PRODUCT_SKU_WITH_WAREHOUSE % (product.default_code, product.warehouse_id.code) for product in with_code: product.external_id = product.default_code for product in other: product.external_id = product.external_id @api.multi def button_submit_product(self): backends = self.mapped('backend_id') for backend in backends: products = self.filtered(lambda p: p.backend_id == backend) products._submit_product() return 1 @api.multi def button_update_inventory(self): backends = self.mapped('backend_id') for backend in backends: products = self.filtered(lambda p: p.backend_id == backend) products._update_inventory() return 1 @api.multi def button_update_price(self): backends = self.mapped('backend_id') for backend in backends: products = self.filtered(lambda p: p.backend_id == backend) products._update_price() return 1 def _submit_product(self): # this should be called on a product set that has the same backend backend = self[0].backend_id with backend.work_on(self._name) as work: exporter = work.component(usage='amazon.product.product.exporter') exporter.run(self) self.write({'date_product_sent': fields.Datetime.now(), 'state': 'sent'}) def _update_inventory(self): # this should be called on a product set that has the same backend backend = self[0].backend_id with backend.work_on(self._name) as work: exporter = work.component(usage='amazon.product.product.exporter') exporter.run_inventory(self) self.write({'date_inventory_sent': fields.Datetime.now()}) def _update_price(self): # this should be called on a product set that has the same backend backend = self[0].backend_id with backend.work_on(self._name) as work: exporter = work.component(usage='amazon.product.product.exporter') exporter.run_price(self) self.write({'date_price_sent': fields.Datetime.now()}) def _update_for_backend_products(self, backend): return self.search([ ('backend_id', '=', backend.id), ('state', '=', 'sent'), ]) def update_inventory(self, backend): products = self._update_for_backend_products(backend) if products: products._update_inventory() def update_price(self, backend): products = self._update_for_backend_products(backend) if products: products._update_price() class ProductProduct(models.Model): _inherit = 'product.product' amazon_bind_ids = fields.One2many('amazon.product.product', 'odoo_id', string='Amazon Listings') class ProductAdapter(Component): _name = 'amazon.product.product.adapter' _inherit = 'amazon.adapter' _apply_on = 'amazon.product.product' def _api(self): return self.api_instance.feeds() def _submit_feed(self, bindings, type, content_type, data): feed_values = { 'backend_id': bindings[0].backend_id.id, 'type': type, 'content_type': content_type, 'data': b64encode(data), } if len(bindings) == 1: feed_values['amazon_product_product_id'] = bindings.id feed = self.env['amazon.feed'].create(feed_values) feed.with_delay(priority=19).submit_feed() # slightly higher than regular submit_feed calls return feed def create(self, bindings): feed_root, _message = self._product_data_feed(bindings) feed_data = self._feed_string(feed_root) self._submit_feed(bindings, 'POST_PRODUCT_DATA', 'text/xml', feed_data) def create_inventory(self, bindings): feed_root, _message = self._product_inventory_feed(bindings) feed_data = self._feed_string(feed_root) self._submit_feed(bindings, 'POST_INVENTORY_AVAILABILITY_DATA', 'text/xml', feed_data) def create_price(self, bindings): feed_root, _message = self._product_price_feed(bindings) feed_data = self._feed_string(feed_root) self._submit_feed(bindings, 'POST_PRODUCT_PRICING_DATA', 'text/xml', feed_data) def _process_product_data(self, bindings): res = [] for amazon_product in bindings: # why iterate? because we probably need more data eventually... if not amazon_product.external_id: raise UserError('Amazon Product Listing (%s) must have an Amazon SKU filled.' % (amazon_product.id, )) res.append({ 'SKU': amazon_product.external_id, }) return res def _product_data_feed(self, bindings): product_datas = self._process_product_data(bindings) root, message = self._feed('Product', bindings[0].backend_id) root.remove(message) self.ElementTree.SubElement(root, 'PurgeAndReplace').text = 'false' for i, product_data in enumerate(product_datas, 1): message = self.ElementTree.SubElement(root, 'Message') self.ElementTree.SubElement(message, 'MessageID').text = str(i) # ElementTree.SubElement(message, 'OperationType').text = 'Update' self.ElementTree.SubElement(message, 'OperationType').text = 'PartialUpdate' product = self.ElementTree.SubElement(message, 'Product') self.ElementTree.SubElement(product, 'SKU').text = product_data['SKU'] # standard_product_id = ElementTree.SubElement(product, 'StandardProductID') # ElementTree.SubElement(standard_product_id, 'Type').text = product_data['StandardProductID.Type'] # ElementTree.SubElement(standard_product_id, 'Value').text = product_data['StandardProductID.Value'] # description_data = ElementTree.SubElement(product, 'DescriptionData') # ElementTree.SubElement(description_data, 'Title').text = product_data['Title'] # ElementTree.SubElement(description_data, 'Brand').text = product_data['Brand'] # ElementTree.SubElement(description_data, 'Description').text = product_data['Description'] # for bullet in product_data['BulletPoints']: # ElementTree.SubElement(description_data, 'BulletPoint').text = bullet # ElementTree.SubElement(description_data, 'Manufacturer').text = product_data['Manufacturer'] # ElementTree.SubElement(description_data, 'ItemType').text = product_data['ItemType'] return root, message def _process_product_inventory(self, bindings): def _qty(binding, buffer_qty): # qty available is all up inventory, less outgoing inventory gives qty to send qty = binding.qty_available - binding.outgoing_qty if binding.buffer_qty >= 0.0: return max((0.0, qty - binding.buffer_qty)) return max((0.0, qty - buffer_qty)) res = [] backend = bindings[0].backend_id backend_warehouses = backend.warehouse_ids backend_fba_warehouses = backend.fba_warehouse_ids warehouses = bindings.mapped('warehouse_id') for warehouse in warehouses: wh_bindings = bindings.filtered(lambda p: p.warehouse_id == warehouse).with_context(warehouse=warehouse.id) buffer_qty = backend.fba_buffer_qty if warehouse in backend_fba_warehouses else backend.buffer_qty for binding in wh_bindings: res.append((binding.external_id, _qty(binding, buffer_qty))) buffer_qty = backend.buffer_qty for binding in bindings.filtered(lambda p: not p.warehouse_id).with_context(warehouse=backend_warehouses.ids): res.append((binding.external_id, _qty(binding, buffer_qty))) return res def _product_inventory_feed(self, bindings): product_datas = self._process_product_inventory(bindings) root, message = self._feed('Inventory', bindings[0].backend_id) root.remove(message) for i, product_data in enumerate(product_datas, 1): sku, qty = product_data message = self.ElementTree.SubElement(root, 'Message') self.ElementTree.SubElement(message, 'MessageID').text = str(i) # ElementTree.SubElement(message, 'OperationType').text = 'Update' self.ElementTree.SubElement(message, 'OperationType').text = 'Update' inventory = self.ElementTree.SubElement(message, 'Inventory') self.ElementTree.SubElement(inventory, 'SKU').text = sku self.ElementTree.SubElement(inventory, 'Quantity').text = str(int(qty)) return root, message def _process_product_price(self, bindings): def _process_product_price_internal(env, binding, pricelist, res): price = binding.lst_price sale_price = None date_start = None date_end = None if pricelist: rule = None sale_price, rule_id = pricelist.get_product_price_rule(binding.odoo_id, 1.0, None) if rule_id: rule = env['product.pricelist.item'].browse(rule_id).exists() if rule and (rule.date_start or rule.date_end): date_start = rule.date_start date_end = rule.date_end res.append((binding.external_id, price, sale_price, date_start, date_end)) res = [] backend = bindings[0].backend_id pricelist = backend.pricelist_id fba_pricelist = backend.fba_pricelist_id backend_fba_warehouses = backend.fba_warehouse_ids fba_bindings = bindings.filtered(lambda b: b.warehouse_id and b.warehouse_id in backend_fba_warehouses) for binding in fba_bindings: _process_product_price_internal(self.env, binding, fba_pricelist, res) for binding in (bindings - fba_bindings): _process_product_price_internal(self.env, binding, pricelist, res) return res def _product_price_feed(self, bindings): backend = bindings[0].backend_id product_datas = self._process_product_price(bindings) root, message = self._feed('Price', backend) root.remove(message) now = fields.Datetime.now() tomorrow = str(fields.Datetime.from_string(now) + timedelta(days=1)) for i, product_data in enumerate(product_datas, 1): sku, _price, _sale_price, date_start, date_end = product_data message = self.ElementTree.SubElement(root, 'Message') self.ElementTree.SubElement(message, 'MessageID').text = str(i) # ElementTree.SubElement(message, 'OperationType').text = 'Update' # self.ElementTree.SubElement(message, 'OperationType').text = 'Update' price = self.ElementTree.SubElement(message, 'Price') self.ElementTree.SubElement(price, 'SKU').text = sku standard_price = self.ElementTree.SubElement(price, 'StandardPrice') standard_price.text = '%0.2f' % (_price, ) standard_price.attrib['currency'] = 'USD' # TODO gather currency if _sale_price and abs(_price - _sale_price) > 0.01: sale = self.ElementTree.SubElement(price, 'Sale') if not date_start: date_start = now self.ElementTree.SubElement(sale, 'StartDate').text = fields.Datetime.from_string(date_start).isoformat() if not date_end: date_end = tomorrow self.ElementTree.SubElement(sale, 'EndDate').text = fields.Datetime.from_string(date_end).isoformat() sale_price = self.ElementTree.SubElement(sale, 'SalePrice') sale_price.text = '%0.2f' % (_sale_price, ) sale_price.attrib['currency'] = 'USD' # TODO gather currency return root, message