first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,668 @@
# 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