Files
tryton/sale.py
T
Jonathan Levy 95bcedd586 Add Python3 support
issue3211
review17131003
2016-03-11 19:13:50 +01:00

292 lines
9.6 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
from decimal import Decimal
from simpleeval import simple_eval
from trytond.pool import PoolMeta, Pool
from trytond.model import ModelSQL, ModelView, MatchMixin, Workflow, fields
from trytond.pyson import Eval, If, Bool
from trytond.transaction import Transaction
from trytond.tools import decistmt
from trytond.modules.product import price_digits
__all__ = ['Sale', 'SaleLine',
'SalePromotion', 'SalePromotion_Product']
class Sale:
__metaclass__ = PoolMeta
__name__ = 'sale.sale'
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, sales):
super(Sale, cls).draft(sales)
# Reset to draft unit price
for sale in sales:
changed = False
for line in sale.lines:
if line.type != 'line':
continue
if line.draft_unit_price is not None:
line.unit_price = line.draft_unit_price
line.draft_unit_price = None
changed = True
if changed:
sale.lines = sale.lines # Trigger changes
cls.save(sales)
@classmethod
@ModelView.button
@Workflow.transition('quotation')
def quote(cls, sales):
super(Sale, cls).quote(sales)
# Store draft unit price before changing it
for sale in sales:
for line in sale.lines:
if line.type != 'line':
continue
if line.draft_unit_price is None:
line.draft_unit_price = line.unit_price
sale.apply_promotion()
cls.save(sales)
def apply_promotion(self):
'Apply promotion'
pool = Pool()
Promotion = pool.get('sale.promotion')
promotions = Promotion.get_promotions(self)
for promotion in promotions:
promotion.apply(self)
class SaleLine:
__metaclass__ = PoolMeta
__name__ = 'sale.line'
draft_unit_price = fields.Numeric('Draft Unit Price',
digits=price_digits, readonly=True)
promotion = fields.Many2One('sale.promotion', 'Promotion',
ondelete='RESTRICT',
domain=[
('company', '=', Eval('_parent_sale', {}).get('company', -1)),
])
class SalePromotion(ModelSQL, ModelView, MatchMixin):
'Sale Promotion'
__name__ = 'sale.promotion'
name = fields.Char('Name', translate=True, required=True)
company = fields.Many2One('company.company', 'Company', required=True,
states={
'readonly': Eval('id', 0) > 0,
},
domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', -1)),
],
select=True)
active = fields.Boolean('Active')
start_date = fields.Date('Start Date',
domain=['OR',
('start_date', '<=', If(~Eval('end_date', None),
datetime.date.max,
Eval('end_date', datetime.date.max))),
('start_date', '=', None),
],
depends=['end_date'])
end_date = fields.Date('End Date',
domain=['OR',
('end_date', '>=', If(~Eval('start_date', None),
datetime.date.min,
Eval('start_date', datetime.date.min))),
('end_date', '=', None),
],
depends=['start_date'])
price_list = fields.Many2One('product.price_list', 'Price List',
ondelete='CASCADE',
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)),
depends=['unit_digits'])
unit = fields.Many2One('product.uom', 'Unit',
states={
'required': Bool(Eval('quantity', 0)),
},
depends=['quantity'])
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'on_change_with_unit_digits')
unit_category = fields.Function(fields.Many2One('product.uom.category',
'Unit Category'), 'on_change_with_unit_category')
products = fields.Many2Many('sale.promotion-product.product',
'promotion', 'product', 'Products',
domain=[
('default_uom_category', '=', Eval('unit_category')),
],
depends=['unit_category'])
formula = fields.Char('Formula', required=True,
help=('Python expression that will be evaluated with:\n'
'- unit_price: the original unit_price'))
@classmethod
def __setup__(cls):
super(SalePromotion, cls).__setup__()
cls._error_messages.update({
'invalid_formula': ('Invalid formula "%(formula)s" '
'in promotion "%(promotion)s" '
'with exception "%(exception)s".'),
})
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_active():
return True
@fields.depends('unit')
def on_change_with_unit_digits(self, name=None):
if self.unit:
return self.unit.digits
return 2
@fields.depends('unit')
def on_change_with_unit_category(self, name=None):
if self.unit:
return self.unit.category.id
@classmethod
def validate(cls, promotions):
super(SalePromotion, cls).validate(promotions)
for promotion in promotions:
promotion.check_formula()
def check_formula(self):
context = self.get_context_formula(None)
try:
if not isinstance(self.get_unit_price(**context), Decimal):
raise ValueError('Not a Decimal')
except Exception, exception:
self.raise_user_error('invalid_formula', {
'formula': self.formula,
'promotion': self.rec_name,
'exception': exception,
})
@classmethod
def _promotions_domain(cls, sale):
return [
['OR',
('start_date', '<=', sale.sale_date),
('start_date', '=', None),
],
['OR',
('end_date', '=', None),
('end_date', '>=', sale.sale_date),
],
['OR',
('price_list', '=', None),
('price_list', '=',
sale.price_list.id if sale.price_list else None),
],
('company', '=', sale.company.id),
]
@classmethod
def get_promotions(cls, sale, pattern=None):
'Yield promotions that apply to sale'
promotions = cls.search(cls._promotions_domain(sale))
if pattern is None:
pattern = {}
for promotion in promotions:
ppattern = pattern.copy()
ppattern.update(promotion.get_pattern(sale))
if promotion.match(ppattern):
yield promotion
def get_pattern(self, sale):
pool = Pool()
Uom = pool.get('product.uom')
pattern = {}
if not self.unit:
return pattern
quantity = 0
for line in sale.lines:
if self.is_valid_sale_line(line):
quantity += Uom.compute_qty(line.unit, line.quantity,
self.unit)
pattern['quantity'] = quantity
return pattern
def match(self, pattern):
if 'quantity' in pattern:
pattern = pattern.copy()
if self.quantity > pattern.pop('quantity'):
return False
return super(SalePromotion, self).match(pattern)
def is_valid_sale_line(self, line):
if self.products:
return line.product in self.products
elif self.unit:
return line.unit.category == self.unit.category
else:
return True
def apply(self, sale):
new_prices = {}
for line in sale.lines:
if line.type != 'line':
continue
if not self.is_valid_sale_line(line):
continue
context = self.get_context_formula(line)
new_prices[line] = self.get_unit_price(**context)
# Apply promotion only if all unit prices decrease
if all(l.unit_price >= p for l, p in new_prices.iteritems()):
for line, unit_price in new_prices.iteritems():
line.unit_price = unit_price
line.promotion = self
sale.lines = sale.lines # Trigger the change
def get_context_formula(self, sale_line):
pool = Pool()
Product = pool.get('product.product')
if sale_line:
with Transaction().set_context(uom=sale_line.unit.id,
currency=sale_line.sale.currency.id):
prices = Product.get_sale_price([sale_line.product])
unit_price = prices[sale_line.product.id]
else:
unit_price = Decimal(0)
return {
'names': {
'unit_price': unit_price,
},
}
def get_unit_price(self, **context):
'Return unit price (as Decimal)'
context.setdefault('functions', {})['Decimal'] = Decimal
return simple_eval(decistmt(self.formula), **context)
class SalePromotion_Product(ModelSQL):
'Sale Promotion - Product'
__name__ = 'sale.promotion-product.product'
promotion = fields.Many2One('sale.promotion', 'Promotion',
required=True, ondelete='CASCADE', select=True)
product = fields.Many2One('product.product', 'Product',
required=True, ondelete='CASCADE')