# 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