Files

439 lines
15 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.
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', [])),
])