mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
294 lines
14 KiB
Python
294 lines
14 KiB
Python
# © 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
|