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

418 lines
14 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 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'})
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(party=party)
sale.company = self.company
sale.warehouse = self.warehouse
sale.web_shop = self
sale.on_change_party()
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)
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', [])),
])