web_shop_woocommerce/web.py

412 lines
14 KiB
Python

# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from decimal import Decimal
import logging
from woocommerce import API
from trytond.exceptions import UserError
from trytond.i18n import gettext, lazy_gettext
from trytond.model import Model, ModelSQL, Unique, fields
from trytond.pyson import Eval
from trytond.pool import PoolMeta, Pool
from trytond.tools import grouped_slice
from trytond.transaction import Transaction
from trytond.modules.product import round_price
from .exceptions import WooCommerceError, MissingParentsError
logger = logging.getLogger(__name__)
class ShopWooCommerceId(ModelSQL):
"Web Shop WooCommerce ID"
__name__ = 'web.shop.woocommerce_id'
record = fields.Reference("Record", 'get_records', required=True)
shop = fields.Many2One('web.shop', "Web Shop", required=True, select=True)
woocommerce_id = fields.Integer("WooCommerce ID", required=True)
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints = [
('record_unique', Unique(t, t.record, t.shop),
'web_shop_woocommerce.msg_id_record_unique'),
]
@classmethod
def get_records(cls):
pool = Pool()
Model = pool.get('ir.model')
models = [klass.__name__ for _, klass in pool.iterobject()
if issubclass(klass, ShopWooCommerceIdMixin)]
models = Model.search([
('model', 'in', models),
])
return [(m.model, m.name) for m in models]
class ShopWooCommerceIdMixin:
woocommerce_id = fields.Function(
fields.Integer(
lazy_gettext('web_shop_woocommerce.msg_woocommerce_id')),
'get_woocommerce_id',
setter='set_woocommerce_id')
@classmethod
def get_woocommerce_id(cls, records, name):
pool = Pool()
WoocommerceID = pool.get('web.shop.woocommerce_id')
result = {}.fromkeys(r.id for r in records)
shop = Transaction().context.get('woocommerce_shop', -1)
for sub_record in grouped_slice(records):
for woo_id in WoocommerceID.search([
('shop', '=', shop),
('record', 'in', map(str, records)),
]):
result[woo_id.record.id] = woo_id.woocommerce_id
return result
@classmethod
def set_woocommerce_id(cls, records, name, value):
pool = Pool()
WooCommerceId = pool.get('web.shop.woocommerce_id')
shop = Transaction().context.get('woocommerce_shop', -1)
if shop < 0:
return
for sub_record in grouped_slice(records):
woo_ids = WooCommerceId.search([
('shop', '=', shop),
('record', '=', map(str, records)),
])
if not woo_ids:
woo_ids = [
WooCommerceId(record=r, shop=shop, woocommerce_id=value)
for r in records]
WooCommerceId.save(woo_ids)
else:
WooCommerceId.write(woo_ids, {'woocommerce_id': value})
@classmethod
def delete(cls, records):
pool = Pool()
WooCommerceId = pool.get('web.shop.woocommerce_id')
for sub_records in grouped_slice(records):
woo_ids = WooCommerceId.search([
('record', 'in', [str(r) for r in sub_records]),
])
if woo_ids:
WooCommerceId.delete(woo_ids)
super().delete(records)
class Shop(metaclass=PoolMeta):
__name__ = 'web.shop'
woocommerce_url = fields.Char(
"WooCommerce URL",
states={
'required': Eval('type') == 'woocommerce',
'invisible': Eval('type') != 'woocommerce',
},
depends=['type'])
woocommerce_consumer_key = fields.Char(
"WooCommerce Consumer Key",
states={
'required': Eval('type') == 'woocommerce',
'invisible': Eval('type') != 'woocommerce',
},
depends=['type'])
woocommerce_consumer_secret = fields.Char(
"WooCommerce Consumer Secret",
states={
'required': Eval('type') == 'woocommerce',
'invisible': Eval('type') != 'woocommerce',
},
depends=['type'])
price_list = fields.Many2One('product.price_list', "Price List")
@classmethod
def __setup__(cls):
super().__setup__()
cls.type.selection.append(('woocommerce', "WooCommerce"))
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//page[@id="woocommerce"]', 'states', {
'invisible': Eval('type') != 'woocommerce',
}),
]
def get_context(self):
context = super().get_context()
if self.type == 'woocommerce':
context['woocommerce_shop'] = self.id
if self.price_list:
context['price_list'] = self.price_list.id
return context
@property
def to_sync(self):
result = super().to_sync
if self.type == 'woocommerce':
result = True
return result
@property
def woocommerce_api_parameters(self):
return {
'url': self.woocommerce_url,
'consumer_key': self.woocommerce_consumer_key,
'consumer_secret': self.woocommerce_consumer_secret,
'timeout': 30,
}
def get_woocommerce_api(self):
return API(**self.woocommerce_api_parameters)
@classmethod
def woocommerce_response(cls, request):
response = request.json()
if 'message' in response:
raise WooCommerceError(
gettext('web_shop_woocommerce.msg_sincronization_error',
response=response['message']))
return response
def woocommerce_tryton_record(self, model, woocommerce_id):
"Return the tryton record of a giveen woocommerce id"
pool = Pool()
WooCommerceID = pool.get('web.shop.woocommerce_id')
if issubclass(model, Model):
model = model.__name__
# TODO: Cache?
table = WooCommerceID.__table__()
query = table.select(table.id,
where=(table.record.like(model + '%%')
& (table.shop == self.id)
& (table.woocommerce_id == woocommerce_id)))
records = WooCommerceID.search([('id', 'in', query)], limit=1)
if records:
return records[0].record
return None
def woocommerce_compare_values(self, Model, woo_values, values):
to_update = {}
for key, value in values.items():
# Do not compare empty categories
if value == []:
continue
woo_value = woo_values.get(key)
if (isinstance(woo_value, list)
and woo_value
and isinstance(woo_value[0], dict)
and isinstance(value[0], dict)
and 'id' in woo_value[0]):
if 'id' in value[0]:
# Use only ids to relation fields
woo_value = [{'id': w['id']} for w in woo_value]
else:
# Only compare keys set on Tryton
tryton_keys = value[0].keys()
woo_value = [dict((k, v)
for k, v in x.items()
if k in tryton_keys
) for x in woo_value]
if woo_value != value:
to_update[key] = value
return to_update
def woocommerce_sync_records(self, Model, records, endpoint):
wcapi = self.get_woocommerce_api()
to_update = {}
latter = []
while records:
for record in records:
entity = record.get_woocommerce_entity()
if entity is None:
latter.append(record)
continue
woo_id = record.woocommerce_id
if not woo_id:
response = self.woocommerce_response(
wcapi.post(endpoint, entity))
record.woocommerce_id = response['id']
else:
to_update[woo_id] = entity
Model.save(records)
Transaction().commit()
if latter and len(records) == len(latter):
raise MissingParentsError(
gettext('web_shop_woocommerce.msg_missing_parents_error',
records=','.join([x.rec_name for x in latter])))
logger.info("Created new records %d/%d", len(records), len(latter))
records = latter
latter = []
logger.info("Getting existing records info")
woo_values = {}
for sub_ids in grouped_slice(to_update.keys(), 100):
params = {
'include': ','.join(map(str, sub_ids)),
}
response = self.woocommerce_response(
wcapi.get(endpoint, params=params))
for woo_record in response:
woo_values[woo_record['id']] = woo_record
logger.info("Comparing and updating values")
for woo_id, values in to_update.items():
woo_values = self.woocommerce_response(
wcapi.get('%s/%s' % (endpoint, woo_id)))
to_update = self.woocommerce_compare_values(
Model, woo_values, values)
if to_update:
self.woocommerce_response(
wcapi.post('%s/%d' % (endpoint, woo_id), to_update))
@classmethod
def woocommerce_update_products(cls, shops=None):
pool = Pool()
Product = pool.get('product.product')
Category = pool.get('product.category')
if shops is None:
shops = cls.search([
('type', '=', 'woocommerce'),
])
cls.lock(shops)
for shop in shops:
with Transaction().set_context(**shop.get_context()):
logger.info("Syncronizing categories for %s", shop.rec_name)
shop.woocommerce_sync_records(
Category,
shop.get_categories(),
'products/categories')
logger.info("Syncronizing products for %s", shop.rec_name)
products, _, _ = shop.get_products()
shop.woocommerce_sync_records(
Product,
products,
'products')
# TODO: Manage removed
# shop.products_removed = []
# shop.categories_removed = []
logger.info("Finised syncronization for %s", shop.rec_name)
cls.save(shops)
def woocommerce_orders_params(self, page):
# XXX: Save last sync date and use that for filtering?
return {
'status': 'on-hold',
'page': page,
}
def woocommerce_customer(self, order):
pool = Pool()
Party = pool.get('party.party')
customer_id = order.get('customer_id', 0)
email = order.get('billing', {}).get('email', '')
if customer_id != 0:
party = self.woocommerce_tryton_record(Party, customer_id)
if party:
return party
elif email:
parties = Party.search([
('contact_mechanisms', 'where', [
('type', '=', 'email'),
('value', '=', email),
]),
], limit=1)
if parties:
return parties[0]
return Party.create_from_woocommerce(self, order)
def woocommerce_sale(self, order):
pool = Pool()
Sale = pool.get('sale.sale')
Currency = pool.get('currency.currency')
sale = Sale()
sale.company = self.company
sale.web_shop = self
sale.web_id = order['id']
sale.reference = order['number']
currencies = Currency.search([
('code', '=', order['currency'])
], limit=1)
if not currencies:
currencies = Currency.search([
('symbol', '=', order['currency_symbol'])
], limit=1)
if not currencies:
raise UserError('missing currency')
sale.currency, = currencies
sale.party = self.woocommerce_customer(order)
# XXX: Addresses
sale.on_change_party()
lines = []
for item in order['line_items']:
line = self.woocommerce_sale_line(order, item, sale)
if line:
lines.append(line)
sale.lines = lines
return sale
def woocommerce_sale_line(self, order, item, sale):
pool = Pool()
Product = pool.get('product.product')
Line = pool.get('sale.line')
line = Line()
line.type = 'line'
line.sale = sale
line.product = self.woocommerce_tryton_record(
Product, item['product_id'])
line.quantity = item['quantity']
line.on_change_product()
line.unit_price = round_price(Decimal(str(item['price'])))
return line
@classmethod
def woocommerce_download_orders(cls, shops=None):
pool = Pool()
Sale = pool.get('sale.sale')
if shops is None:
shops = cls.search([
('type', '=', 'woocommerce'),
])
cls.lock(shops)
sales = []
for shop in shops:
with Transaction().set_context(**shop.get_context()):
wcapi = shop.get_woocommerce_api()
page = 1
orders = shop.woocommerce_response(
wcapi.get(
'orders',
params=shop.woocommerce_orders_params(page)))
while orders:
for order in orders:
sale = shop.woocommerce_sale(order)
if sale:
sales.append(sale)
page += 1
orders = shop.woocommerce_response(
wcapi.get(
'orders',
params=shop.woocommerce_orders_params(page)))
# Store last updated date
Sale.save(sales)
cls.save(shops)