669 lines
22 KiB
Python
669 lines
22 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||
# this repository contains the full copyright notices and license terms.
|
||
from functools import wraps
|
||
|
||
import elasticsearch
|
||
from elasticsearch import VERSION as ES_VERSION
|
||
from elasticsearch import Elasticsearch
|
||
|
||
from trytond.exceptions import RateLimitException
|
||
from trytond.i18n import gettext, lazy_gettext
|
||
from trytond.model import ModelSQL, Unique, fields
|
||
from trytond.pool import Pool, PoolMeta
|
||
from trytond.pyson import Eval
|
||
|
||
from .exceptions import BadRequest, LoginException, NotFound
|
||
|
||
|
||
def migrate_doc_type(func):
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
if ES_VERSION >= (7,):
|
||
kwargs = kwargs.copy()
|
||
doc_type = kwargs.pop('doc_type')
|
||
kwargs['index'] += '_' + doc_type
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
|
||
|
||
class VSFElasticsearch(Elasticsearch):
|
||
|
||
@migrate_doc_type
|
||
def index(self, **kwargs):
|
||
return super().index(**kwargs)
|
||
|
||
@migrate_doc_type
|
||
def delete(self, **kwargs):
|
||
return super().delete(**kwargs)
|
||
|
||
|
||
def join_name(firstname, lastname):
|
||
# Use unbreakable spaces in firstname
|
||
# to prevent to split on them
|
||
firstname = firstname.replace(' ', ' ')
|
||
return ' '.join([firstname, lastname])
|
||
|
||
|
||
def split_name(name):
|
||
return (name.split(' ', 1) + [''])[:2]
|
||
|
||
|
||
def issubdict(dct, other):
|
||
for key, value in dct.items():
|
||
if value != other.get(key):
|
||
return False
|
||
return True
|
||
|
||
|
||
def with_user(required=False):
|
||
def decorator(func):
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
pool = Pool()
|
||
User = pool.get('web.user')
|
||
token = kwargs.pop('token', None)
|
||
if token:
|
||
user = User.get_user(token)
|
||
else:
|
||
user = kwargs.get('user')
|
||
if required and not user:
|
||
raise LoginException()
|
||
kwargs['user'] = user
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
return decorator
|
||
|
||
|
||
class Shop(metaclass=PoolMeta):
|
||
__name__ = 'web.shop'
|
||
|
||
vsf_elasticsearch_url = fields.Char(
|
||
"Elasticsearch URL",
|
||
states={
|
||
'required': Eval('type') == 'vsf',
|
||
'invisible': Eval('type') != 'vsf',
|
||
})
|
||
vsf_elasticsearch_index = fields.Char(
|
||
"Elasticsearch Index",
|
||
states={
|
||
'required': Eval('type') == 'vsf',
|
||
'invisible': Eval('type') != 'vsf',
|
||
})
|
||
|
||
@classmethod
|
||
def __setup__(cls):
|
||
super().__setup__()
|
||
cls.type.selection.append(('vsf', "Vue Storefront"))
|
||
|
||
@classmethod
|
||
def default_vsf_elasticsearch_url(cls):
|
||
return 'http://localhost:9200/'
|
||
|
||
@classmethod
|
||
def default_vsf_elasticsearch_index(cls):
|
||
return 'vue_storefront_catalog'
|
||
|
||
@classmethod
|
||
def view_attributes(cls):
|
||
return super().view_attributes() + [
|
||
('//page[@id="vsf"]', 'states', {
|
||
'invisible': Eval('type') != 'vsf',
|
||
}),
|
||
]
|
||
|
||
@property
|
||
def to_sync(self):
|
||
result = super().to_sync
|
||
if self.type == 'vsf':
|
||
result = True
|
||
return result
|
||
|
||
def get_vsf_elasticsearch(self):
|
||
return VSFElasticsearch(self.vsf_elasticsearch_url)
|
||
|
||
@classmethod
|
||
def vsf_update(cls, shops=None):
|
||
pool = Pool()
|
||
Product = pool.get('product.product')
|
||
ProductTemplate = pool.get('product.template')
|
||
Category = pool.get('product.category')
|
||
try:
|
||
ProductAttribute = pool.get('product.attribute')
|
||
except KeyError:
|
||
ProductAttribute = None
|
||
if shops is None:
|
||
shops = cls.search([
|
||
('type', '=', 'vsf'),
|
||
])
|
||
cls.lock(shops)
|
||
for shop in shops:
|
||
es = shop.get_vsf_elasticsearch()
|
||
if ProductAttribute:
|
||
attributes = shop.get_attributes()
|
||
ProductAttribute.set_vsf_identifier(attributes)
|
||
categories = shop.get_categories()
|
||
Category.set_vsf_identifier(categories)
|
||
products, prices, taxes = shop.get_products()
|
||
Product.set_vsf_identifier(products)
|
||
|
||
templates = set()
|
||
for product in products:
|
||
if product.vsf_is_configurable(shop):
|
||
templates.add(product.template)
|
||
continue
|
||
entity = product.get_vsf_entity(
|
||
shop, price=prices[product.id], tax=taxes[product.id])
|
||
es.index(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='product',
|
||
id=product.vsf_identifier.id,
|
||
body=entity)
|
||
templates = ProductTemplate.browse(templates)
|
||
ProductTemplate.set_vsf_identifier(templates)
|
||
for template in templates:
|
||
template_products = template.get_vsf_products(shop)
|
||
price, tax = min(
|
||
(prices[p.id], taxes[p.id]) for p in template_products
|
||
if prices[p.id] is not None and taxes[p.id] is not None)
|
||
entity = template.get_vsf_entity(shop, price=price, tax=tax)
|
||
entity['configurable_children'] = [
|
||
product.get_vsf_entity(
|
||
shop, price=prices[product.id], tax=taxes[product.id])
|
||
for product in template_products]
|
||
es.index(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='product',
|
||
id=template.vsf_identifier.id,
|
||
body=entity)
|
||
|
||
for category in categories:
|
||
entity = category.get_vsf_entity(shop)
|
||
es.index(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='category',
|
||
id=category.vsf_identifier.id,
|
||
body=entity)
|
||
|
||
if ProductAttribute:
|
||
for attribute in attributes:
|
||
entity = attribute.get_vsf_entity(shop)
|
||
es.index(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='attribute',
|
||
id=attribute.vsf_identifier.id,
|
||
body=entity)
|
||
|
||
for product in shop.products_removed:
|
||
template = product.template
|
||
if template.vsf_identifier:
|
||
template_products = product.template.get_vsf_products(shop)
|
||
if not template_products:
|
||
try:
|
||
es.delete(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='product',
|
||
id=template.vsf_identifier.id)
|
||
except elasticsearch.exceptions.NotFoundError:
|
||
pass
|
||
if product.vsf_identifier:
|
||
try:
|
||
es.delete(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='product',
|
||
id=product.vsf_identifier.id)
|
||
except elasticsearch.exceptions.NotFoundError:
|
||
pass
|
||
shop.products_removed = []
|
||
|
||
for category in shop.categories_removed:
|
||
if category.vsf_identifier:
|
||
try:
|
||
es.delete(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='category',
|
||
id=category.vsf_identifier.id)
|
||
except elasticsearch.exceptions.NotFoundError:
|
||
pass
|
||
shop.categories_removed = []
|
||
|
||
if ProductAttribute:
|
||
for attribute in shop.attributes_removed:
|
||
if attribute.vsf_identifier:
|
||
try:
|
||
es.delete(
|
||
index=shop.vsf_elasticsearch_index,
|
||
doc_type='attribute',
|
||
id=attribute.vsf_identifier.id)
|
||
except elasticsearch.exceptions.NotFoundError:
|
||
pass
|
||
shop.attributes_removed = []
|
||
|
||
cls.save(shops)
|
||
|
||
def POST_vsf_user_create(self, data):
|
||
pool = Pool()
|
||
User = pool.get('web.user')
|
||
Party = pool.get('party.party')
|
||
firstname = data['customer']['firstname']
|
||
lastname = data['customer']['lastname']
|
||
email = data['customer']['email']
|
||
user = User(email=email, password=data['password'])
|
||
party = Party(name=join_name(firstname, lastname))
|
||
party.save()
|
||
user.party = party
|
||
user.save()
|
||
firstname, lastname = split_name(user.party.name)
|
||
return {
|
||
'email': user.email,
|
||
'firstname': firstname,
|
||
'lastname': lastname,
|
||
'addresses': [],
|
||
}
|
||
|
||
def POST_vsf_user_login(self, data):
|
||
pool = Pool()
|
||
User = pool.get('web.user')
|
||
try:
|
||
user = User.authenticate(data['username'], data['password'])
|
||
except RateLimitException:
|
||
raise LoginException(gettext(
|
||
'web_shop_vue_storefront.msg_login_wrong'))
|
||
if user:
|
||
return user.new_session()
|
||
else:
|
||
raise LoginException(gettext(
|
||
'web_shop_vue_storefront.msg_login_wrong'))
|
||
|
||
def POST_vsf_user_reset_password(self, data):
|
||
pool = Pool()
|
||
User = pool.get('web.user')
|
||
users = User.search([
|
||
('email', '=', data['email']),
|
||
])
|
||
User.reset_password(users)
|
||
|
||
@with_user(required=True)
|
||
def POST_vsf_user_change_password(self, data, user):
|
||
pool = Pool()
|
||
User = pool.get('web.user')
|
||
try:
|
||
user = User.authenticate(user.email, data['currentPassword'])
|
||
except RateLimitException:
|
||
raise LoginException(gettext(
|
||
'web_shop_vue_storefront.msg_login_wrong'))
|
||
if user:
|
||
user.password = data['newPassword']
|
||
user.save()
|
||
else:
|
||
raise LoginException(gettext(
|
||
'web_shop_vue_storefront.msg_login_wrong'))
|
||
|
||
@with_user(required=True)
|
||
def GET_vsf_user_order_history(
|
||
self, data, user, pageSize='20', currentPage='1'):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
try:
|
||
pageSize = int(pageSize)
|
||
currentPage = int(currentPage)
|
||
except ValueError:
|
||
raise BadRequest()
|
||
sales = Sale.search([
|
||
('party', '=', user.party),
|
||
('state', 'in', ['confirmed', 'processing', 'done']),
|
||
],
|
||
offset=pageSize * (currentPage - 1),
|
||
limit=pageSize,
|
||
order=[
|
||
('sale_date', 'DESC'),
|
||
('id', 'DESC'),
|
||
])
|
||
items = []
|
||
for sale in sales:
|
||
items.append(sale.get_vsf_user_order_history())
|
||
return {'items': items}
|
||
|
||
@with_user(required=True)
|
||
def GET_vsf_user_me(self, data, user):
|
||
return user.get_vsf()
|
||
|
||
@with_user(required=True)
|
||
def POST_vsf_user_me(self, data, user):
|
||
user.set_vsf(data['customer'])
|
||
user.save()
|
||
return user.get_vsf()
|
||
|
||
def GET_vsf_stock_check(self, data, sku):
|
||
try:
|
||
return self.GET_vsf_stock_list(data, sku)[0]
|
||
except IndexError:
|
||
raise NotFound()
|
||
|
||
def GET_vsf_stock_list(self, data, skus):
|
||
pool = Pool()
|
||
Product = pool.get('product.product')
|
||
Template = pool.get('product.template')
|
||
skus = skus.split(',')
|
||
products = Product.search([
|
||
('vsf_sku', 'in', skus),
|
||
('web_shops', '=', self.id),
|
||
])
|
||
products += Template.search([
|
||
('vsf_sku', 'in', skus),
|
||
('products.web_shops', '=', self.id),
|
||
])
|
||
return [p.get_vsf_stock() for p in products]
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_create(self, data, user=None):
|
||
party = user.party if user else None
|
||
sale = self.get_sale(party)
|
||
sale.save()
|
||
return sale.vsf_id
|
||
|
||
@with_user()
|
||
def GET_vsf_cart_pull(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
return [line.get_vsf() for line in sale.lines if line.product]
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_update(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
SaleLine = pool.get('sale.line')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
if data['cartItem'].get('item_id'):
|
||
line = SaleLine(data['cartItem']['item_id'])
|
||
if line.sale != sale:
|
||
raise BadRequest()
|
||
else:
|
||
line = SaleLine(sale=sale)
|
||
line.set_vsf(data['cartItem'])
|
||
line.save()
|
||
return line.get_vsf()
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_delete(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
SaleLine = pool.get('sale.line')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
line = SaleLine(data['cartItem']['item_id'])
|
||
if line.sale != sale:
|
||
raise BadRequest()
|
||
SaleLine.delete([line])
|
||
return True
|
||
|
||
@with_user()
|
||
def GET_vsf_cart_totals(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
return sale.get_vsf()
|
||
|
||
@with_user()
|
||
def GET_vsf_cart_payment_methods(self, data, cartId, user=None):
|
||
return []
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_shipping_methods(self, data, cartId, user=None):
|
||
return []
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_shipping_information(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
return sale.get_vsf()
|
||
|
||
@with_user(required=True)
|
||
def POST_vsf_order_create(self, data, cartId, user):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
self.vsf_order_create(data, sale, user)
|
||
return 'OK'
|
||
|
||
def vsf_order_create(self, data, sale, user):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
SaleLine = pool.get('sale.line')
|
||
sale.set_vsf(data, user)
|
||
sku2lines = {
|
||
line.product.vsf_sku: line for line in sale.lines if line.product}
|
||
for product in data.get('products', []):
|
||
sku = product['sku']
|
||
line = sku2lines.get(sku)
|
||
if not line:
|
||
line = SaleLine(sale=sale)
|
||
sku2lines[sku] = line
|
||
line.set_vsf(product)
|
||
sale.lines = sku2lines.values()
|
||
sale.save()
|
||
Sale.quote([sale])
|
||
payment_method = data['addressInformation']['payment_method_code']
|
||
if payment_method == 'cashondelivery':
|
||
Sale.confirm([sale])
|
||
return sale
|
||
|
||
|
||
class ShopCoupon(metaclass=PoolMeta):
|
||
__name__ = 'web.shop'
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_apply_coupon(self, data, cartId, coupon, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
PromotionCouponNumber = pool.get('sale.promotion.coupon.number')
|
||
try:
|
||
coupon, = PromotionCouponNumber.search([
|
||
('number', 'ilike', coupon),
|
||
], limit=1)
|
||
except ValueError:
|
||
return False
|
||
sale.coupons = [coupon]
|
||
sale.save()
|
||
return True
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_delete_coupon(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
sale.coupons = []
|
||
sale.save()
|
||
return True
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_coupon(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
if sale.coupons:
|
||
return sale.coupons[0].number
|
||
else:
|
||
return ''
|
||
|
||
|
||
class ShopShipmentCost(metaclass=PoolMeta):
|
||
__name__ = 'web.shop'
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_shipping_methods(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
methods = super().POST_vsf_cart_shipping_methods(
|
||
data, cartId, user=user)
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
sale.set_vsf_shipping_methods(data)
|
||
for carrier in sale.available_carriers:
|
||
method = carrier.get_vsf()
|
||
sale.carrier = carrier
|
||
method['price_incl_tax'] = sale.compute_shipment_cost(carrier)
|
||
methods.append(method)
|
||
return methods
|
||
|
||
@with_user()
|
||
def POST_vsf_cart_shipping_information(self, data, cartId, user=None):
|
||
pool = Pool()
|
||
Sale = pool.get('sale.sale')
|
||
sale = Sale.search_vsf(cartId, self, user)
|
||
sale.set_vsf(data, user)
|
||
sale.save()
|
||
return super().POST_vsf_cart_shipping_information(
|
||
data, cartId, user=user)
|
||
|
||
|
||
class ShopVSFIdentifier(ModelSQL):
|
||
__name__ = 'web.shop.vsf_identifier'
|
||
|
||
record = fields.Reference("Record", 'get_records', required=True)
|
||
|
||
@classmethod
|
||
def __setup__(cls):
|
||
super().__setup__()
|
||
t = cls.__table__()
|
||
cls._sql_constraints = [
|
||
('record_unique', Unique(t, t.record),
|
||
'web_shop_vue_storefront.msg_identifier_record_unique'),
|
||
]
|
||
|
||
@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, ShopVSFIdentifierMixin))
|
||
return [(m, get_name(m)) for m in models]
|
||
|
||
|
||
class ShopVSFIdentifierMixin:
|
||
__slots__ = ()
|
||
vsf_identifier = fields.Many2One(
|
||
'web.shop.vsf_identifier',
|
||
lazy_gettext('web_shop_vue_storefront.msg_vsf_identifier'),
|
||
ondelete='RESTRICT', readonly=True)
|
||
|
||
@classmethod
|
||
def set_vsf_identifier(cls, records):
|
||
pool = Pool()
|
||
Identifier = pool.get('web.shop.vsf_identifier')
|
||
vsf_identifiers = []
|
||
for record in records:
|
||
if not record.vsf_identifier:
|
||
record.vsf_identifier = Identifier(record=record)
|
||
vsf_identifiers.append(record.vsf_identifier)
|
||
Identifier.save(vsf_identifiers)
|
||
cls.save(records)
|
||
|
||
|
||
class User(metaclass=PoolMeta):
|
||
__name__ = 'web.user'
|
||
|
||
def get_vsf(self):
|
||
if not self.party:
|
||
return {
|
||
'email': self.email,
|
||
}
|
||
firstname, lastname = split_name(self.party.name)
|
||
data = {
|
||
'email': self.email,
|
||
'firstname': firstname,
|
||
'lastname': lastname,
|
||
'addresses': (
|
||
[a.get_vsf() for a in self.party.addresses]
|
||
+ [a.get_vsf(self.party) for p in self.secondary_parties
|
||
for a in p.addresses if p != self.party]),
|
||
}
|
||
|
||
default_billing = self.invoice_address
|
||
if not default_billing:
|
||
default_billing = self.party.address_get('invoice')
|
||
if default_billing:
|
||
data['default_billing'] = default_billing.id
|
||
|
||
default_shipping = self.shipment_address
|
||
if not default_shipping:
|
||
default_shipping = self.party.address_get('delivery')
|
||
if default_shipping:
|
||
data['default_shipping'] = default_shipping.id
|
||
|
||
return data
|
||
|
||
def set_vsf(self, data):
|
||
pool = Pool()
|
||
Party = pool.get('party.party')
|
||
Address = pool.get('party.address')
|
||
self.email = data['email']
|
||
if not self.party:
|
||
self.party = Party()
|
||
self.party.name = join_name(data['firstname'], data['lastname'])
|
||
|
||
default_billing = None
|
||
default_shipping = None
|
||
addresses = []
|
||
for address_data in data['addresses']:
|
||
address = self.set_vsf_address(address_data, self.party)
|
||
addresses.append(address)
|
||
if ((address.id and address.id == data.get('default_billing'))
|
||
or address_data.get('default_billing')):
|
||
default_billing = address
|
||
if ((address.id and address.id == data.get('default_shipping'))
|
||
or address_data.get('default_shipping')):
|
||
default_shipping = address
|
||
|
||
if address_data.get('id'):
|
||
if address_data['id'] != address.id:
|
||
address = Address(address_data['id'])
|
||
if (address.party != self.party
|
||
and address.party not in self.secondary_parties):
|
||
raise BadRequest()
|
||
address.active = False
|
||
addresses.append(address)
|
||
Address.save(addresses)
|
||
self.invoice_address = default_billing
|
||
self.shipment_address = default_shipping
|
||
|
||
def set_vsf_address(self, address_data, party):
|
||
pool = Pool()
|
||
Party = pool.get('party.party')
|
||
Address = pool.get('party.address')
|
||
Identifier = pool.get('party.identifier')
|
||
|
||
addresses = self.party.addresses
|
||
for party in self.secondary_parties:
|
||
addresses += party.addresses
|
||
|
||
for address in addresses:
|
||
if issubdict(address.get_vsf(party), address_data):
|
||
return address
|
||
|
||
address = Address()
|
||
party = address.party = self.party
|
||
if address_data.get('company'):
|
||
for company_party in self.secondary_parties:
|
||
tax_code = (
|
||
company_party.tax_identifier.code
|
||
if company_party.tax_identifier else '')
|
||
if (company_party.name == address_data['company']
|
||
and (not address_data.get('vat_id')
|
||
or tax_code == address_data['vat_id'])):
|
||
break
|
||
else:
|
||
identifier = Identifier()
|
||
identifier.set_vsf_tax_identifier(
|
||
address_data['vat_id'])
|
||
company_party = Party(
|
||
name=address_data['company'],
|
||
identifiers=[identifier])
|
||
company_party.save()
|
||
self.secondary_parties += (company_party,)
|
||
self.save()
|
||
address.party = company_party
|
||
address.set_vsf(address_data, party)
|
||
return address
|