Files
tradon/modules/web_shop_vue_storefront/web.py
2026-03-14 09:42:12 +00:00

669 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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