# 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 from collections import defaultdict from sql import Literal from sql.operators import Equal from trytond.cache import Cache from trytond.model import ( DeactivableMixin, Exclude, ModelSQL, ModelView, fields) from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval from trytond.transaction import Transaction class Many2ManyInactive(fields.Many2Many): def get(self, ids, model, name, values=None): with Transaction().set_context(inactive_test=True): return super().get(ids, model, name, values=values) def set(self, Model, name, ids, values, *args): with Transaction().set_context(inactive_test=True): return super().set(Model, name, ids, values, *args) class Inactivate(ModelSQL, DeactivableMixin): @classmethod def search_domain(cls, domain, active_test=None, tables=None): context = Transaction().context if context.get('inactive_test'): domain = [domain, ('active', '=', False)] return super().search_domain( domain, active_test=active_test, tables=tables) @classmethod def on_modification(cls, mode, records, field_names=None): super().on_modification(mode, records, field_names=field_names) if mode == 'delete': cls.copy([r for r in records if r.active and r.shop.to_sync], default={ 'active': False, }) class Shop(DeactivableMixin, ModelSQL, ModelView): __name__ = 'web.shop' name = fields.Char("Name", required=True) company = fields.Many2One('company.company', "Company", required=True) currency = fields.Many2One('currency.currency', "Currency", required=True) language = fields.Many2One( 'ir.lang', "Language", domain=[ ('translatable', '=', True), ]) type = fields.Selection([ (None, ""), ], "Type", help="The front-end used for the web shop.") warehouses = fields.Many2Many( 'web.shop-stock.location', 'shop', 'warehouse', "Warehouses", domain=[ ('type', '=', 'warehouse'), ]) guest_party = fields.Many2One( 'party.party', "Guest Party", context={ 'company': Eval('company', -1), }, depends={'company'}) sale_draft_abandon_delay = fields.TimeDelta( "Sale Draft Abandon Delay", domain=['OR', ('sale_draft_abandon_delay', '>=', dt.timedelta()), ('sale_draft_abandon_delay', '=', None), ], help="The time period after which a sale made via the web shop " "without modification is cancelled.\n" "Leave empty to never cancel any draft sales.") countries = fields.Many2Many( 'web.shop-country.country', 'shop', 'country', "Countries") products = fields.Many2Many( 'web.shop-product.product', 'shop', 'product', "Products", domain=[ ('salable', '=', True), ], context={ 'company': Eval('company', -1), }, depends={'company'}, help="The list of products to publish.") products_removed = Many2ManyInactive( 'web.shop-product.product', 'shop', 'product', "Products Removed", context={ 'company': Eval('company', -1), }, depends={'company'}, help="The list of products to unpublish.") categories = fields.Many2Many( 'web.shop-product.category', 'shop', 'category', "Categories", context={ 'company': Eval('company', -1), }, depends={'company'}, help="The list of categories to publish.") categories_removed = Many2ManyInactive( 'web.shop-product.category', 'shop', 'category', "Categories Removed", context={ 'company': Eval('company', -1), }, depends={'company'}, help="The list of categories to unpublish.") _name_cache = Cache('web.shop.name', context=False) @property def warehouse(self): if self.warehouses: return self.warehouses[0] @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_constraints = [ ('name_unique', Exclude(t, (t.name, Equal), where=t.active == Literal(True)), 'web_shop.msg_shop_name_unique'), ] @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_currency(cls): pool = Pool() Company = pool.get('company.company') company_id = cls.default_company() if company_id is not None and company_id >= 0: company = Company(company_id) return company.currency.id @classmethod def get(cls, name): shop_id = cls._name_cache.get(name) if not shop_id: shop, = cls.search([('name', '=', name)]) cls._name_cache.set(name, shop.id) else: shop = cls(shop_id) return shop @classmethod def copy(cls, shops, default=None): default = default.copy() if default is not None else {} default.setdefault('warehouses') default.setdefault('products') default.setdefault('products_removed') default.setdefault('categories') default.setdefault('categories_removed') return super().copy(shops, default=default) @classmethod def on_modification(cls, mode, shops, field_names=None): super().on_modification(mode, shops, field_names=field_names) if mode == 'delete' or (mode == 'write' and 'name' in field_names): cls._name_cache.clear() @property def to_sync(self): return False def _customer_taxe_rule(self): pool = Pool() Configuration = pool.get('account.configuration') config = Configuration(1) return config.get_multivalue( 'default_customer_tax_rule', company=self.company.id) def get_context(self): pool = Pool() Date = pool.get('ir.date') with Transaction().set_context(company=self.company.id): today = Date.today() return { 'language': self.language.code if self.language else None, 'company': self.company.id, 'currency': self.currency.id, 'locations': [w.id for w in self.warehouses], 'stock_date_end': today, 'stock_assign': True, } def get_products(self, pattern=None, key=None): """Return a list of products with their corresponding dictionaries of prices and taxes according to the tax pattern. If a key function is supplied, the products will be sorted in ascending order based on the key applied to each product.""" pool = Pool() Date = pool.get('ir.date') Product = pool.get('product.product') Tax = pool.get('account.tax') if pattern is None: pattern = {} with Transaction().set_context(**self.get_context()): if key is not None: all_products = sorted(self.products, key=key) else: all_products = self.products all_products = Product.browse(all_products) today = Date.today() customer_tax_rule = self._customer_taxe_rule() taxes2products = defaultdict(list) for product in all_products: taxes = set() for tax in product.customer_taxes_used: if customer_tax_rule: tax_ids = customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.update(tax_ids) continue taxes.add(tax.id) if customer_tax_rule: tax_ids = customer_tax_rule.apply(None, pattern) if tax_ids: taxes.update(tax_ids) taxes2products[tuple(sorted(taxes))].append(product) prices, taxes = {}, {} for tax_ids, products in taxes2products.items(): with Transaction().set_context(**self.get_context()): products = Product.browse(products) taxes_ = Tax.browse(tax_ids) with Transaction().set_context(taxes=tax_ids): prices.update(Product.get_sale_price(products)) for product in products: price = prices[product.id] if price is not None: taxes[product.id] = sum( t['amount'] for t in Tax.compute( taxes_, price, 1, today)) else: taxes[product.id] = None return all_products, prices, taxes def get_categories(self): "Return the list of categories" pool = Pool() Category = pool.get('product.category') with Transaction().set_context(**self.get_context()): return Category.browse(self.categories) def get_sale(self, party=None): pool = Pool() Sale = pool.get('sale.sale') if not party: party = self.guest_party sale = Sale() sale.company = self.company sale.warehouse = self.warehouse sale.web_shop = self if party: sale.party = party sale.invoice_address = party.address_get(type='invoice') with Transaction().set_context( warehouse=self.warehouse.id if self.warehouse else None): sale.shipment_address = party.address_get(type='delivery') sale.on_change_web_shop() sale.currency = self.currency sale.invoice_method = 'order' sale.shipment_method = 'order' return sale def update_sale_ids(self, sale_ids): pool = Pool() Sale = pool.get('sale.sale') return self.update_sales(Sale.browse(sale_ids)) def update_sales(self, sales): assert all(s.web_shop == self for s in sales) @property def is_managing_gift_card(self): "Return if the webshop manages gift cards" pass class Shop_PriceList(metaclass=PoolMeta): __name__ = 'web.shop' sale_price_list = fields.Many2One( 'product.price_list', "Sale Price List", domain=[ ('company', '=', Eval('company', -1)), ], help="The price list to compute sale price of products.") non_sale_price_list = fields.Many2One( 'product.price_list', "Non-Sale Price List", domain=[ ('company', '=', Eval('company', -1)), ], help="The price list to compute the price of products " "when it is not on sale.") def get_context(self): context = super().get_context() if self.sale_price_list: context['price_list'] = self.sale_price_list.id if (self.non_sale_price_list and Transaction().context.get('_non_sale_price', False)): context['price_list'] = self.non_sale_price_list.id return context def get_sale(self, party=None): sale = super().get_sale(party=party) if self.sale_price_list: sale.price_list = self.sale_price_list return sale class Shop_ShipmentCost(metaclass=PoolMeta): __name__ = 'web.shop' def get_sale(self, party=None): sale = super().get_sale(party=party) sale.shipment_cost_method = 'order' return sale class Shop_TaxRuleCountry(metaclass=PoolMeta): __name__ = 'web.shop' def get_products(self, pattern=None, key=None): pattern = pattern.copy() if pattern is not None else {} if (self.warehouse and self.warehouse.address and self.warehouse.address.country): pattern.setdefault( 'from_country', self.warehouse.address.country.id) else: pattern.setdefault('from_country') pattern.setdefault('to_country') return super().get_products(pattern=pattern, key=key) class Shop_Warehouse(ModelSQL): __name__ = 'web.shop-stock.location' shop = fields.Many2One( 'web.shop', "Shop", ondelete='CASCADE', required=True) warehouse = fields.Many2One( 'stock.location', "Warehouse", ondelete='CASCADE', required=True, domain=[ ('type', '=', 'warehouse'), ]) class Shop_Country(ModelSQL): __name__ = 'web.shop-country.country' shop = fields.Many2One( 'web.shop', "Shop", ondelete='CASCADE', required=True) country = fields.Many2One( 'country.country', "Country", ondelete='CASCADE', required=True) class Shop_Product(Inactivate): __name__ = 'web.shop-product.product' shop = fields.Many2One( 'web.shop', "Shop", ondelete='CASCADE', required=True) product = fields.Many2One( 'product.product', "Product", ondelete='RESTRICT', required=True) class Shop_ProductCategory(Inactivate): __name__ = 'web.shop-product.category' shop = fields.Many2One( 'web.shop', "Shop", ondelete='CASCADE', required=True) category = fields.Many2One( 'product.category', "Category", ondelete='RESTRICT', required=True) class ShopAttribute(metaclass=PoolMeta): __name__ = 'web.shop' attributes = fields.Many2Many( 'web.shop-product.attribute', 'shop', 'attribute', "Attributes", help="The list of attributes to publish.") attributes_removed = Many2ManyInactive( 'web.shop-product.attribute', 'shop', 'attribute', "Attributes Removed", help="The list of attributes to unpublish.") def get_attributes(self): "Return the list of attributes" pool = Pool() Attribute = pool.get('product.attribute') with Transaction().set_context(**self.get_context()): return Attribute.browse(self.attributes) class Shop_Attribute(Inactivate): __name__ = 'web.shop-product.attribute' shop = fields.Many2One( 'web.shop', "Shop", ondelete='CASCADE', required=True) attribute = fields.Many2One( 'product.attribute', "Attribute", ondelete='RESTRICT', required=True) class User(metaclass=PoolMeta): __name__ = 'web.user' invoice_address = fields.Many2One( 'party.address', "Invoice Address", domain=['OR', ('party', '=', Eval('party', -1)), ('party', 'in', Eval('secondary_parties', [])), ]) shipment_address = fields.Many2One( 'party.address', "Shipment Address", domain=['OR', ('party', '=', Eval('party', -1)), ('party', 'in', Eval('secondary_parties', [])), ])