718 lines
28 KiB
Python
Executable File
718 lines
28 KiB
Python
Executable File
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime as dt
|
|
import urllib.parse
|
|
from decimal import Decimal
|
|
|
|
import pyactiveresource
|
|
import shopify
|
|
from shopify.api_version import ApiVersion
|
|
|
|
from trytond.cache import Cache
|
|
from trytond.config import config
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval
|
|
from trytond.tools import grouped_slice
|
|
from trytond.transaction import Transaction
|
|
from trytond.url import http_host
|
|
|
|
from .common import IdentifierMixin, IdentifiersMixin
|
|
from .exceptions import ShopifyError
|
|
|
|
EDIT_ORDER_DELAY = dt.timedelta(days=60 + 1)
|
|
|
|
|
|
class Shop(metaclass=PoolMeta):
|
|
__name__ = 'web.shop'
|
|
|
|
_states = {
|
|
'required': Eval('type') == 'shopify',
|
|
'invisible': Eval('type') != 'shopify',
|
|
}
|
|
|
|
shopify_url = fields.Char("Shop URL", states=_states)
|
|
shopify_version = fields.Selection(
|
|
'get_shopify_versions', "Version", states=_states)
|
|
shopify_password = fields.Char("Access Token", states=_states, strip=False)
|
|
shopify_webhook_shared_secret = fields.Char(
|
|
"Webhook Shared Secret", strip=False,
|
|
states={
|
|
'invisible': _states['invisible'],
|
|
})
|
|
shopify_webhook_endpoint_order = fields.Function(
|
|
fields.Char(
|
|
"Webhook Order Endpoint",
|
|
help="The URL to be called by Shopify for Order events."),
|
|
'on_change_with_shopify_webhook_endpoint_order')
|
|
shopify_warehouses = fields.One2Many(
|
|
'web.shop-stock.location', 'shop', "Warehouses", states=_states)
|
|
shopify_payment_journals = fields.One2Many(
|
|
'web.shop.shopify_payment_journal', 'shop', "Payment Journals",
|
|
states=_states)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.type.selection.append(('shopify', "Shopify"))
|
|
invisible = Eval('type') == 'shopify'
|
|
for field in [cls.attributes, cls.attributes_removed, cls.warehouses]:
|
|
if field.states.get('invisible'):
|
|
field.states['invisible'] |= invisible
|
|
else:
|
|
field.states['invisible'] = invisible
|
|
|
|
@classmethod
|
|
def get_shopify_versions(cls):
|
|
return [(None, "")] + sorted(
|
|
((v, v) for v in ApiVersion.versions), reverse=True)
|
|
|
|
@fields.depends('name')
|
|
def on_change_with_shopify_webhook_endpoint_order(self, name=None):
|
|
if not self.name:
|
|
return
|
|
url_part = {
|
|
'database_name': Transaction().database.name,
|
|
'shop': self.name,
|
|
}
|
|
return http_host() + (
|
|
urllib.parse.quote(
|
|
'/%(database_name)s/web_shop_shopify/webhook/%(shop)s/order' %
|
|
url_part))
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('//page[@id="shopify"]', 'states', {
|
|
'invisible': Eval('type') != 'shopify',
|
|
}),
|
|
]
|
|
|
|
@classmethod
|
|
def validate_fields(cls, shops, field_names):
|
|
super().validate_fields(shops, field_names)
|
|
if field_names & {'type', 'products'}:
|
|
for shop in shops:
|
|
if shop.type == 'shopify':
|
|
for product in shop.products:
|
|
shop._shopify_check_product(product)
|
|
|
|
def _shopify_check_product(self, product):
|
|
if not product.template.shopify_uom:
|
|
shopify_uom = product.template.get_shopify_uom()
|
|
if shopify_uom.digits:
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.'
|
|
'msg_product_shopify_uom_digits',
|
|
product=product.rec_name))
|
|
|
|
@property
|
|
def to_sync(self):
|
|
result = super().to_sync
|
|
if self.type == 'shopify':
|
|
result = True
|
|
return result
|
|
|
|
def get_sale(self, party=None):
|
|
sale = super().get_sale(party=party)
|
|
if self.type == 'shopify':
|
|
sale.invoice_method = 'shipment'
|
|
return sale
|
|
|
|
def shopify_session(self):
|
|
return shopify.Session.temp(
|
|
self.shopify_url, self.shopify_version, self.shopify_password)
|
|
|
|
def get_payment_journal(self, currency_code, pattern):
|
|
for payment_journal in self.shopify_payment_journals:
|
|
if (payment_journal.journal.currency.code == currency_code
|
|
and payment_journal.match(pattern)):
|
|
return payment_journal.journal
|
|
|
|
def managed_metafields(self):
|
|
return set()
|
|
|
|
def __sync_metafields(self, resource, metafields):
|
|
metafields = metafields.copy()
|
|
managed_metafields = self.managed_metafields()
|
|
assert metafields.keys() <= managed_metafields
|
|
for metafield in resource.metafields():
|
|
key = '.'.join([metafield.namespace, metafield.key])
|
|
value = metafield.to_dict()
|
|
if key not in metafields:
|
|
if key in managed_metafields:
|
|
metafield.destroy()
|
|
elif metafields[key] != value:
|
|
for k, v in metafields.pop(key).items():
|
|
setattr(metafield, k, v)
|
|
metafield.save()
|
|
for key, value in metafields.items():
|
|
namespace, key = key.split('.', 1)
|
|
value['namespace'] = namespace
|
|
value['key'] = key
|
|
resource.add_metafield(shopify.Metafield(value))
|
|
|
|
@classmethod
|
|
def shopify_update_product(cls, shops=None):
|
|
"""Update Shopify Products
|
|
|
|
The transaction is committed after the creation of each new resource.
|
|
"""
|
|
pool = Pool()
|
|
InventoryItem = pool.get('product.shopify_inventory_item')
|
|
transaction = Transaction()
|
|
if shops is None:
|
|
shops = cls.search([
|
|
('type', '=', 'shopify'),
|
|
])
|
|
for shop in shops:
|
|
with shop.shopify_session():
|
|
shopify_shop = shopify.Shop.current()
|
|
shop_language = (
|
|
shop.language.code if shop.language
|
|
else transaction.language)
|
|
categories = shop.get_categories()
|
|
products, prices, taxes = shop.get_products()
|
|
if shopify_shop.currency.lower() != shop.currency.code.lower():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_shop_currency_different',
|
|
shop=shop.rec_name,
|
|
shop_currency=shop.currency.code,
|
|
shopify_currency=shopify_shop.currency))
|
|
if (shopify_shop.primary_locale.lower()
|
|
!= shop_language.lower()):
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_shop_locale_different',
|
|
shop=shop.rec_name,
|
|
shop_language=shop_language,
|
|
shopify_primary_locale=shopify_shop.primary_locale
|
|
))
|
|
|
|
for category in categories:
|
|
shop.__shopify_update_category(category)
|
|
|
|
categories = set(categories)
|
|
inventory_items = InventoryItem.browse(products)
|
|
for product, inventory_item in zip(products, inventory_items):
|
|
price = prices[product.id]
|
|
tax = taxes[product.id]
|
|
|
|
template = product.template
|
|
if not template.shopify_uom:
|
|
shop._shopify_check_product(product)
|
|
template.shopify_uom = template.get_shopify_uom()
|
|
template.save()
|
|
|
|
shop.__shopify_update_template(
|
|
shopify_shop, categories, template,
|
|
product, price, tax)
|
|
shop.__shopify_update_product(
|
|
shopify_shop, product, price, tax)
|
|
shop.__shopify_update_inventory_item(inventory_item)
|
|
|
|
for category in shop.categories_removed:
|
|
shop.__shopify_remove_category(category)
|
|
shop.categories_removed = []
|
|
|
|
products = set(products)
|
|
for product in shop.products_removed:
|
|
template = product.template
|
|
if set(template.products).isdisjoint(products):
|
|
shop.__shopify_remove_template(template)
|
|
else:
|
|
shop.__shopify_remove_product(product)
|
|
shop.products_removed = []
|
|
cls.save(shops)
|
|
|
|
def __shopify_update_category(self, category):
|
|
if not category.is_shopify_to_update(self):
|
|
return
|
|
custom_collection = category.get_shopify(self)
|
|
if not custom_collection.save():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_custom_collection_fail',
|
|
category=category.rec_name,
|
|
error="\n".join(
|
|
custom_collection.errors.full_messages())))
|
|
identifier = category.set_shopify_identifier(
|
|
self, custom_collection.id)
|
|
if identifier.to_update:
|
|
identifier.to_update = False
|
|
identifier.save()
|
|
Transaction().commit()
|
|
|
|
self.__sync_metafields(
|
|
custom_collection, category.get_shopify_metafields(self))
|
|
|
|
def __shopify_remove_category(self, category):
|
|
shopify_id = category.get_shopify_identifier(self)
|
|
if shopify_id:
|
|
if shopify.CustomCollection.exists(shopify_id):
|
|
shopify.CustomCollection.find(shopify_id).destroy()
|
|
category.set_shopify_identifier(self)
|
|
|
|
def __shopify_update_template(
|
|
self, shopify_shop, categories, template, product, price, tax):
|
|
if not template.is_shopify_to_update(self):
|
|
return
|
|
shopify_product = template.get_shopify(self)
|
|
new = shopify_product.is_new()
|
|
if new:
|
|
shopify_product.variants = [
|
|
product.get_shopify(
|
|
self, price, tax,
|
|
shop_taxes_included=shopify_shop.taxes_included,
|
|
shop_weight_unit=shopify_shop.weight_unit)]
|
|
else:
|
|
# Set fake value for missing new options
|
|
for j, variant in enumerate(shopify_product.variants):
|
|
for i, _ in range(len(shopify_product.options), 1):
|
|
name = 'option%i' % i
|
|
if not getattr(variant, name, None):
|
|
setattr(variant, name, '_option%i-%i' % (i, j))
|
|
if not shopify_product.save():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_product_fail',
|
|
template=template.rec_name,
|
|
error="\n".join(shopify_product.errors.full_messages())))
|
|
identifier = template.set_shopify_identifier(
|
|
self, shopify_product.id)
|
|
if identifier.to_update:
|
|
identifier.to_update = False
|
|
identifier.save()
|
|
if new:
|
|
variant, = shopify_product.variants
|
|
product.set_shopify_identifier(self, variant.id)
|
|
Transaction().commit()
|
|
|
|
self.__sync_metafields(
|
|
shopify_product, template.get_shopify_metafields(self))
|
|
|
|
collection_ids = {
|
|
c.id for c in shopify_product.collections()}
|
|
for category in template.categories_all:
|
|
while category:
|
|
if category in categories:
|
|
custom_collection = (
|
|
shopify.CustomCollection.find(
|
|
category.get_shopify_identifier(
|
|
self)))
|
|
if custom_collection.id in collection_ids:
|
|
collection_ids.remove(
|
|
custom_collection.id)
|
|
else:
|
|
shopify_product.add_to_collection(
|
|
custom_collection)
|
|
category = category.parent
|
|
for collection_id in collection_ids:
|
|
collection = shopify.CustomCollection.find(
|
|
collection_id)
|
|
shopify_product.remove_from_collection(collection)
|
|
|
|
self.__shopify_update_images(template, shopify_product)
|
|
|
|
def __shopify_remove_template(self, template):
|
|
shopify_id = template.get_shopify_identifier(self)
|
|
if not shopify_id:
|
|
return
|
|
if shopify.Product.exists(shopify_id):
|
|
shopify.Product.find(shopify_id).destroy()
|
|
template.set_shopify_identifier(self)
|
|
for product in template.products:
|
|
product.set_shopify_identifier(self)
|
|
if getattr(template, 'images', None):
|
|
for image in template.images:
|
|
image.set_shopify_identifier(self)
|
|
|
|
def __shopify_update_images(self, template, shopify_product):
|
|
if not getattr(template, 'images', None):
|
|
return
|
|
transaction = Transaction()
|
|
image_ids = set()
|
|
for i, image in enumerate(filter(
|
|
lambda i: i.web_shop,
|
|
template.images_used), 1):
|
|
product_image = image.get_shopify(self)
|
|
new_image = not product_image.id
|
|
product_image.position = i
|
|
if not product_image.save():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify'
|
|
'.msg_product_image_fail',
|
|
image=image.rec_name,
|
|
template=template.rec_name,
|
|
error="\n".join(
|
|
product_image.errors
|
|
.full_messages())))
|
|
image_ids.add(product_image.id)
|
|
if new_image:
|
|
image.set_shopify_identifier(
|
|
self, product_image.id)
|
|
transaction.commit()
|
|
|
|
for image in shopify_product.images:
|
|
if image.id not in image_ids:
|
|
image.destroy()
|
|
|
|
def __shopify_update_product(self, shopify_shop, product, price, tax):
|
|
update_extra = {'price': str(price), 'tax': str(tax)}
|
|
if not product.is_shopify_to_update(self, **update_extra):
|
|
return
|
|
variant = product.get_shopify(
|
|
self, price, tax,
|
|
shop_taxes_included=shopify_shop.taxes_included,
|
|
shop_weight_unit=shopify_shop.weight_unit)
|
|
if not variant.save():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_variant_fail',
|
|
product=product.rec_name,
|
|
error="\n".join(variant.errors.full_messages())
|
|
))
|
|
identifier = product.set_shopify_identifier(self, variant.id)
|
|
if identifier.to_update or identifier.to_update_extra != update_extra:
|
|
identifier.to_update = False
|
|
identifier.to_update_extra = update_extra
|
|
identifier.save()
|
|
Transaction().commit()
|
|
|
|
self.__sync_metafields(variant, product.get_shopify_metafields(self))
|
|
|
|
def __shopify_update_inventory_item(self, inventory_item):
|
|
if not inventory_item.is_shopify_to_update(self):
|
|
return
|
|
shopify_inventory_item = inventory_item.get_shopify(self)
|
|
if shopify_inventory_item:
|
|
if not shopify_inventory_item.save():
|
|
raise ShopifyError(gettext(
|
|
'web_shop_shopify.msg_inventory_item_fail',
|
|
product=inventory_item.product.rec_name,
|
|
error="\n".join(
|
|
inventory_item.errors.full_messages())))
|
|
identifier = inventory_item.set_shopify_identifier(
|
|
self, shopify_inventory_item.id if
|
|
shopify_inventory_item.tracked else None)
|
|
if identifier and identifier.to_update:
|
|
identifier.to_update = False
|
|
identifier.save()
|
|
Transaction().commit()
|
|
|
|
def __shopify_remove_product(self, product):
|
|
shopify_id = product.get_shopify_identifier(self)
|
|
if shopify_id:
|
|
if shopify.Variant.exists(shopify_id):
|
|
shopify.Variant.find(shopify_id).destroy()
|
|
product.set_shopify_identifier(self)
|
|
|
|
@classmethod
|
|
def shopify_update_inventory(cls, shops=None):
|
|
"""Update Shopify Inventory"""
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
if shops is None:
|
|
shops = cls.search([
|
|
('type', '=', 'shopify'),
|
|
])
|
|
for shop in shops:
|
|
for shop_warehouse in shop.shopify_warehouses:
|
|
location_id = shop_warehouse.shopify_id
|
|
if not location_id:
|
|
continue
|
|
location_id = int(location_id)
|
|
with Transaction().set_context(
|
|
shop.get_context(),
|
|
**shop_warehouse.get_shopify_inventory_context()):
|
|
products = Product.browse([
|
|
p for p in shop.products if p.shopify_uom])
|
|
with shop.shopify_session():
|
|
shop.__shopify_update_inventory(products, location_id)
|
|
|
|
def __shopify_update_inventory(self, products, location_id):
|
|
pool = Pool()
|
|
InventoryItem = pool.get('product.shopify_inventory_item')
|
|
inventory_items = InventoryItem.browse(products)
|
|
product2quantity = {p.id: int(p.shopify_quantity) for p in products}
|
|
shopify2product = {
|
|
i.get_shopify_identifier(self): i.id for i in inventory_items}
|
|
shopify2product.pop(None, None)
|
|
product2shopify = {v: k for k, v in shopify2product.items()}
|
|
|
|
location = shopify.Location.find(location_id)
|
|
for i, inventory_level in enumerate(
|
|
location.inventory_levels(limit=250, no_iter_next=False)):
|
|
inventory_item_id = inventory_level.inventory_item_id
|
|
product_id = shopify2product.get(inventory_item_id)
|
|
if product_id is None:
|
|
continue
|
|
quantity = product2quantity.pop(product_id)
|
|
if inventory_level.available != quantity:
|
|
try:
|
|
shopify.InventoryLevel.set(
|
|
location_id, inventory_item_id, quantity)
|
|
except pyactiveresource.connection.ResourceNotFound:
|
|
pass
|
|
|
|
for product_id, quantity in product2quantity.items():
|
|
inventory_item_id = product2shopify.get(product_id)
|
|
if inventory_item_id is None:
|
|
continue
|
|
try:
|
|
shopify.InventoryLevel.set(
|
|
location_id, inventory_item_id, quantity)
|
|
except pyactiveresource.connection.ResourceNotFound:
|
|
pass
|
|
|
|
@classmethod
|
|
def shopify_fetch_order(cls, shops=None):
|
|
"""Fetch new Shopify Order"""
|
|
pool = Pool()
|
|
Sale = pool.get('sale.sale')
|
|
Payment = pool.get('account.payment')
|
|
context = Transaction().context
|
|
if shops is None:
|
|
shops = cls.search([
|
|
('type', '=', 'shopify'),
|
|
])
|
|
cls.lock(shops)
|
|
for shop in shops:
|
|
last_sales = Sale.search([
|
|
('web_shop', '=', shop.id),
|
|
], order=[('shopify_identifier_signed', 'DESC')], limit=1)
|
|
if last_sales:
|
|
last_sale, = last_sales
|
|
last_order_id = last_sale.shopify_identifier
|
|
else:
|
|
last_order_id = ''
|
|
with shop.shopify_session():
|
|
if 'shopify_orders' in context:
|
|
orders = shopify.Order.find(
|
|
ids=context['shopify_orders'],
|
|
limit=250, no_iter_next=False)
|
|
else:
|
|
orders = shopify.Order.find(
|
|
status='open', since_id=last_order_id,
|
|
limit=250, no_iter_next=False)
|
|
sales = []
|
|
for i, order in enumerate(orders):
|
|
sales.append(Sale.get_from_shopify(shop, order))
|
|
Sale.save(sales)
|
|
for sale, order in zip(sales, orders):
|
|
sale.shopify_tax_adjustment = (
|
|
Decimal(order.total_price) - sale.total_amount)
|
|
Sale.save(sales)
|
|
Sale.quote(sales)
|
|
for sale, order in zip(sales, orders):
|
|
Payment.get_from_shopify(sale, order)
|
|
|
|
@classmethod
|
|
def shopify_update_order(cls, shops=None):
|
|
"""Update existing sale from Shopify"""
|
|
pool = Pool()
|
|
Sale = pool.get('sale.sale')
|
|
if shops is None:
|
|
shops = cls.search([
|
|
('type', '=', 'shopify'),
|
|
])
|
|
cls.lock(shops)
|
|
now = dt.datetime.now()
|
|
for shop in shops:
|
|
sales = Sale.search([
|
|
('web_shop', '=', shop.id),
|
|
('shopify_identifier', '!=', None),
|
|
['OR',
|
|
('state', 'in',
|
|
['quotation', 'confirmed', 'processing']),
|
|
('create_date', '>=', now - EDIT_ORDER_DELAY),
|
|
],
|
|
])
|
|
for sub_sales in grouped_slice(sales, count=250):
|
|
cls._shopify_update_order(shop, list(sub_sales))
|
|
|
|
@classmethod
|
|
def _shopify_update_order(cls, shop, sales):
|
|
assert shop.type == 'shopify'
|
|
assert all(s.web_shop == shop for s in sales)
|
|
with shop.shopify_session():
|
|
orders = shopify.Order.find(
|
|
ids=','.join(str(s.shopify_identifier) for s in sales),
|
|
status='any')
|
|
id2order = {o.id: o for o in orders}
|
|
|
|
to_update = []
|
|
orders = []
|
|
for sale in sales:
|
|
try:
|
|
order = id2order[sale.shopify_identifier]
|
|
except KeyError:
|
|
continue
|
|
to_update.append(sale)
|
|
orders.append(order)
|
|
cls.shopify_update_sale(to_update, orders)
|
|
|
|
@classmethod
|
|
def shopify_update_sale(cls, sales, orders):
|
|
"""Update sales based on Shopify orders"""
|
|
pool = Pool()
|
|
Amendment = pool.get('sale.amendment')
|
|
Payment = pool.get('account.payment')
|
|
Sale = pool.get('sale.sale')
|
|
assert len(sales) == len(orders)
|
|
to_update = {}
|
|
for sale, order in zip(sales, orders):
|
|
assert sale.shopify_identifier == order.id
|
|
shop = sale.web_shop
|
|
with shop.shopify_session():
|
|
sale = Sale.get_from_shopify(shop, order, sale=sale)
|
|
if sale._changed_values:
|
|
sale.untaxed_amount_cache = None
|
|
sale.tax_amount_cache = None
|
|
sale.total_amount_cache = None
|
|
to_update[sale] = order
|
|
Payment.get_from_shopify(sale, order)
|
|
Sale.save(to_update.keys())
|
|
for sale, order in to_update.items():
|
|
sale.shopify_tax_adjustment = (
|
|
Decimal(order.current_total_price) - sale.total_amount)
|
|
Sale.store_cache(to_update.keys())
|
|
Amendment._clear_sale(to_update.keys())
|
|
Sale.__queue__.process(to_update.keys())
|
|
|
|
|
|
class ShopShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView):
|
|
"Shopify Identifier"
|
|
__name__ = 'web.shop.shopify_identifier'
|
|
|
|
record = fields.Reference("Record", 'get_records', required=True)
|
|
web_shop = fields.Many2One(
|
|
'web.shop', "Web Shop", required=True, ondelete='CASCADE')
|
|
to_update = fields.Boolean("To Update")
|
|
to_update_extra = fields.Dict(None, "To Update Extra")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.shopify_identifier_signed.states = {
|
|
'required': True,
|
|
}
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('record_web_shop_unique',
|
|
Unique(t, t.record, t.shopify_identifier_signed),
|
|
'web_shop_shopify.msg_identifier_record_web_shop_unique'),
|
|
]
|
|
cls._buttons.update({
|
|
'set_to_update': {},
|
|
})
|
|
|
|
@classmethod
|
|
def get_records(cls):
|
|
pool = Pool()
|
|
Model = pool.get('ir.model')
|
|
get_name = Model.get_name
|
|
models = (klass.__name__ for _, klass in pool.iterobject()
|
|
if issubclass(klass, IdentifiersMixin))
|
|
return [(m, get_name(m)) for m in models]
|
|
|
|
@classmethod
|
|
def set_to_update(cls, identifiers):
|
|
cls.write(identifiers, {'to_update': True})
|
|
|
|
|
|
class Shop_Warehouse(ModelView, metaclass=PoolMeta):
|
|
__name__ = 'web.shop-stock.location'
|
|
|
|
shopify_stock_skip_warehouse = fields.Boolean(
|
|
"Only storage zone",
|
|
help="Check to use only the quantity of the storage zone.")
|
|
shopify_id = fields.Selection(
|
|
'get_shopify_locations', "Shopify ID")
|
|
_shopify_locations_cache = Cache(
|
|
__name__ + '.get_shopify_locations',
|
|
duration=config.getint(
|
|
'web_shop_shopify', 'locations_cache', default=15 * 60),
|
|
context=False)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('shop')
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('shopify_id_unique',
|
|
Unique(t, t.shopify_id),
|
|
'web_shop_shopify.msg_location_id_unique'),
|
|
]
|
|
|
|
@fields.depends(
|
|
'shop', '_parent_shop.shopify_url', '_parent_shop.shopify_version',
|
|
'_parent_shop.shopify_password')
|
|
def get_shopify_locations(self):
|
|
locations = [(None, "")]
|
|
if self.shop:
|
|
locations_cache = self._shopify_locations_cache.get(self.shop.id)
|
|
if locations_cache is not None:
|
|
return locations_cache
|
|
try:
|
|
with self.shop.shopify_session():
|
|
locations += [
|
|
(str(l.id), l.name)
|
|
for l in shopify.Location.find(no_iter_next=False)]
|
|
self._shopify_locations_cache.set(self.shop.id, locations)
|
|
except (AttributeError,
|
|
shopify.VersionNotFoundError,
|
|
pyactiveresource.connection.Error):
|
|
pass
|
|
return locations
|
|
|
|
def get_shopify_inventory_context(self):
|
|
return {
|
|
'locations': [self.warehouse.id],
|
|
'stock_skip_warehouse': self.shopify_stock_skip_warehouse,
|
|
'with_childs': True,
|
|
}
|
|
|
|
|
|
class Shop_Attribute(metaclass=PoolMeta):
|
|
__name__ = 'web.shop-product.attribute'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
domain = [
|
|
('type', '!=', 'shopify'),
|
|
]
|
|
if cls.shop.domain:
|
|
cls.shop.domain = [cls.shop.domain, domain]
|
|
else:
|
|
cls.shop.domain = domain
|
|
|
|
|
|
class ShopShopifyPaymentJournal(
|
|
sequence_ordered(), MatchMixin, ModelSQL, ModelView):
|
|
"Shopify Payment Journal"
|
|
__name__ = 'web.shop.shopify_payment_journal'
|
|
|
|
shop = fields.Many2One(
|
|
'web.shop', "Shop", required=True, ondelete='CASCADE',
|
|
domain=[
|
|
('type', '=', 'shopify'),
|
|
])
|
|
gateway = fields.Char(
|
|
"Gateway",
|
|
help="The payment gateway name for which the journal must be used.")
|
|
journal = fields.Many2One(
|
|
'account.payment.journal', "Journal", required=True,
|
|
domain=[
|
|
('process_method', '=', 'shopify'),
|
|
])
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('shop')
|
|
|
|
# TODO: add wizard to export translations
|