2014-12-02 12:55:47 +01:00
|
|
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
|
|
|
# this repository contains the full copyright notices and license terms.
|
2013-09-23 11:53:12 +02:00
|
|
|
import datetime
|
2023-08-17 16:26:09 +02:00
|
|
|
from collections import defaultdict
|
2014-11-24 14:02:57 +01:00
|
|
|
|
2016-09-13 14:08:49 +02:00
|
|
|
from sql import Literal
|
2023-08-17 16:26:09 +02:00
|
|
|
from sql.aggregate import Count, Max
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2018-12-29 14:20:29 +01:00
|
|
|
from trytond.i18n import gettext
|
2021-01-17 01:24:33 +01:00
|
|
|
from trytond.model import (
|
2022-10-11 00:44:50 +02:00
|
|
|
DeactivableMixin, Index, MatchMixin, ModelSQL, ModelView, fields,
|
2021-12-11 17:58:43 +01:00
|
|
|
sequence_ordered)
|
2021-08-21 09:09:56 +02:00
|
|
|
from trytond.modules.currency.fields import Monetary
|
2023-08-17 17:57:11 +02:00
|
|
|
from trytond.modules.product import price_digits, round_price
|
2021-12-11 17:58:43 +01:00
|
|
|
from trytond.pool import Pool, PoolMeta
|
2022-11-24 19:16:51 +01:00
|
|
|
from trytond.pyson import Bool, Eval, If, TimeDelta
|
2023-08-17 16:26:09 +02:00
|
|
|
from trytond.tools import (
|
|
|
|
|
grouped_slice, is_full_text, lstrip_wildcard, reduce_ids)
|
2021-12-11 17:58:43 +01:00
|
|
|
from trytond.transaction import Transaction
|
2015-01-06 23:30:57 +01:00
|
|
|
|
2018-12-29 14:20:29 +01:00
|
|
|
from .exceptions import PurchaseUOMWarning
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2018-06-26 19:11:51 +02:00
|
|
|
class Template(metaclass=PoolMeta):
|
2013-09-23 11:53:12 +02:00
|
|
|
__name__ = "product.template"
|
2020-03-17 20:08:12 +01:00
|
|
|
purchasable = fields.Boolean("Purchasable")
|
2019-03-15 11:30:21 +01:00
|
|
|
product_suppliers = fields.One2Many(
|
|
|
|
|
'purchase.product_supplier', 'template', "Suppliers",
|
|
|
|
|
states={
|
2013-09-23 11:53:12 +02:00
|
|
|
'invisible': (~Eval('purchasable', False)
|
2013-10-09 18:25:44 +02:00
|
|
|
| ~Eval('context', {}).get('company')),
|
2019-03-15 11:30:21 +01:00
|
|
|
},
|
2021-01-17 01:24:33 +01:00
|
|
|
domain=[
|
|
|
|
|
If(~Eval('active'), ('active', '=', False), ()),
|
2022-04-08 19:07:13 +02:00
|
|
|
])
|
2023-08-20 12:17:27 +02:00
|
|
|
purchase_uom = fields.Many2One(
|
|
|
|
|
'product.uom', "Purchase UoM",
|
|
|
|
|
states={
|
2013-09-23 11:53:12 +02:00
|
|
|
'invisible': ~Eval('purchasable'),
|
|
|
|
|
'required': Eval('purchasable', False),
|
|
|
|
|
},
|
2023-08-20 12:17:27 +02:00
|
|
|
domain=[('category', '=', Eval('default_uom_category'))],
|
|
|
|
|
help="The default Unit of Measure for purchases.")
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2014-01-26 14:29:28 +01:00
|
|
|
@fields.depends('default_uom', 'purchase_uom', 'purchasable')
|
2014-10-10 10:03:32 +02:00
|
|
|
def on_change_default_uom(self):
|
|
|
|
|
try:
|
2014-10-29 20:40:09 +01:00
|
|
|
super(Template, self).on_change_default_uom()
|
2014-10-10 10:03:32 +02:00
|
|
|
except AttributeError:
|
2014-10-29 20:40:09 +01:00
|
|
|
pass
|
2013-09-23 11:53:12 +02:00
|
|
|
if self.default_uom:
|
|
|
|
|
if self.purchase_uom:
|
2014-10-29 20:40:09 +01:00
|
|
|
if self.default_uom.category != self.purchase_uom.category:
|
|
|
|
|
self.purchase_uom = self.default_uom
|
2013-09-23 11:53:12 +02:00
|
|
|
else:
|
2014-10-29 20:40:09 +01:00
|
|
|
self.purchase_uom = self.default_uom
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2015-03-09 19:42:02 +01:00
|
|
|
@classmethod
|
|
|
|
|
def view_attributes(cls):
|
|
|
|
|
return super(Template, cls).view_attributes() + [
|
|
|
|
|
('//page[@id="suppliers"]', 'states', {
|
|
|
|
|
'invisible': ~Eval('purchasable'),
|
|
|
|
|
})]
|
|
|
|
|
|
2019-03-15 11:30:21 +01:00
|
|
|
def product_suppliers_used(self, **pattern):
|
2019-10-24 19:08:07 +02:00
|
|
|
# Skip rules to test pattern on all records
|
|
|
|
|
with Transaction().set_user(0):
|
|
|
|
|
template = self.__class__(self)
|
|
|
|
|
for product_supplier in template.product_suppliers:
|
2019-03-15 11:30:21 +01:00
|
|
|
if product_supplier.match(pattern):
|
|
|
|
|
yield product_supplier
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
@classmethod
|
2013-12-19 14:45:20 +01:00
|
|
|
def write(cls, *args):
|
2018-12-29 14:20:29 +01:00
|
|
|
pool = Pool()
|
|
|
|
|
Warning = pool.get('res.user.warning')
|
2013-12-19 14:45:20 +01:00
|
|
|
actions = iter(args)
|
|
|
|
|
for templates, values in zip(actions, actions):
|
|
|
|
|
if not values.get("purchase_uom"):
|
|
|
|
|
continue
|
2013-09-23 11:53:12 +02:00
|
|
|
for template in templates:
|
|
|
|
|
if not template.purchase_uom:
|
|
|
|
|
continue
|
2013-12-19 14:45:20 +01:00
|
|
|
if template.purchase_uom.id == values["purchase_uom"]:
|
2013-09-23 11:53:12 +02:00
|
|
|
continue
|
|
|
|
|
for product in template.products:
|
|
|
|
|
if not product.product_suppliers:
|
|
|
|
|
continue
|
2018-12-29 14:20:29 +01:00
|
|
|
name = '%s@product_template' % template.id
|
|
|
|
|
if Warning.check(name):
|
|
|
|
|
raise PurchaseUOMWarning(
|
|
|
|
|
name, gettext('purchase.msg_change_purchase_uom'))
|
2013-12-19 14:45:20 +01:00
|
|
|
super(Template, cls).write(*args)
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2020-05-01 20:31:02 +02:00
|
|
|
@classmethod
|
|
|
|
|
def copy(cls, templates, default=None):
|
|
|
|
|
pool = Pool()
|
|
|
|
|
ProductSupplier = pool.get('purchase.product_supplier')
|
|
|
|
|
if default is None:
|
|
|
|
|
default = {}
|
|
|
|
|
else:
|
|
|
|
|
default = default.copy()
|
|
|
|
|
|
|
|
|
|
copy_suppliers = 'product_suppliers' not in default
|
|
|
|
|
default.setdefault('product_suppliers', None)
|
|
|
|
|
new_templates = super().copy(templates, default)
|
|
|
|
|
if copy_suppliers:
|
|
|
|
|
old2new = {}
|
|
|
|
|
to_copy = []
|
|
|
|
|
for template, new_template in zip(templates, new_templates):
|
|
|
|
|
to_copy.extend(
|
|
|
|
|
ps for ps in template.product_suppliers if not ps.product)
|
|
|
|
|
old2new[template.id] = new_template.id
|
|
|
|
|
if to_copy:
|
|
|
|
|
ProductSupplier.copy(to_copy, {
|
|
|
|
|
'template': lambda d: old2new[d['template']],
|
|
|
|
|
})
|
|
|
|
|
return new_templates
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2018-06-26 19:11:51 +02:00
|
|
|
class Product(metaclass=PoolMeta):
|
2013-09-23 11:53:12 +02:00
|
|
|
__name__ = 'product.product'
|
|
|
|
|
|
2019-03-15 11:30:21 +01:00
|
|
|
product_suppliers = fields.One2Many(
|
|
|
|
|
'purchase.product_supplier', 'product', "Suppliers",
|
|
|
|
|
domain=[
|
|
|
|
|
('template', '=', Eval('template')),
|
2021-01-17 01:24:33 +01:00
|
|
|
If(~Eval('active'), ('active', '=', False), ()),
|
2019-03-15 11:30:21 +01:00
|
|
|
],
|
|
|
|
|
states={
|
|
|
|
|
'invisible': (~Eval('purchasable', False)
|
|
|
|
|
| ~Eval('context', {}).get('company')),
|
2022-04-08 19:07:13 +02:00
|
|
|
})
|
2017-04-02 12:52:15 +02:00
|
|
|
purchase_price_uom = fields.Function(fields.Numeric(
|
|
|
|
|
"Purchase Price", digits=price_digits), 'get_purchase_price_uom')
|
2023-08-17 16:26:09 +02:00
|
|
|
last_purchase_price_uom = fields.Function(fields.Numeric(
|
|
|
|
|
"Last Purchase Price", digits=price_digits),
|
|
|
|
|
'get_last_purchase_price_uom')
|
2017-04-02 12:52:15 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_purchase_price_uom(cls, products, name):
|
2017-10-30 09:56:52 +01:00
|
|
|
quantity = Transaction().context.get('quantity') or 0
|
2017-04-02 12:52:15 +02:00
|
|
|
return cls.get_purchase_price(products, quantity=quantity)
|
|
|
|
|
|
2023-08-17 16:26:09 +02:00
|
|
|
@classmethod
|
|
|
|
|
def get_last_purchase_price_uom(cls, products, name=None):
|
|
|
|
|
pool = Pool()
|
|
|
|
|
Company = pool.get('company.company')
|
|
|
|
|
Currency = pool.get('currency.currency')
|
|
|
|
|
Date = pool.get('ir.date')
|
|
|
|
|
Line = pool.get('purchase.line')
|
|
|
|
|
Purchase = pool.get('purchase.purchase')
|
|
|
|
|
UoM = pool.get('product.uom')
|
|
|
|
|
|
|
|
|
|
transaction = Transaction()
|
|
|
|
|
context = transaction.context
|
|
|
|
|
cursor = transaction.connection.cursor()
|
|
|
|
|
purchase = Purchase.__table__()
|
|
|
|
|
line = Line.__table__()
|
|
|
|
|
|
|
|
|
|
line_ids = []
|
|
|
|
|
where = purchase.state.in_(['confirmed', 'processing', 'done'])
|
|
|
|
|
supplier = context.get('supplier')
|
|
|
|
|
if supplier:
|
|
|
|
|
where &= purchase.party == supplier
|
|
|
|
|
for sub_products in grouped_slice(products):
|
|
|
|
|
query = (line
|
|
|
|
|
.join(purchase, condition=line.purchase == purchase.id)
|
|
|
|
|
.select(
|
|
|
|
|
Max(line.id),
|
|
|
|
|
where=where & reduce_ids(
|
|
|
|
|
line.product, map(int, sub_products)),
|
|
|
|
|
group_by=[line.product]))
|
|
|
|
|
cursor.execute(*query)
|
|
|
|
|
line_ids.extend(i for i, in cursor)
|
|
|
|
|
lines = Line.browse(line_ids)
|
|
|
|
|
|
|
|
|
|
uom = None
|
|
|
|
|
if context.get('uom'):
|
|
|
|
|
uom = UoM(context['uom'])
|
|
|
|
|
|
|
|
|
|
currency = None
|
|
|
|
|
if context.get('currency'):
|
|
|
|
|
currency = Currency(context['currency'])
|
|
|
|
|
elif context.get('company'):
|
|
|
|
|
currency = Company(context['company']).currency
|
|
|
|
|
date = context.get('purchase_date') or Date.today()
|
|
|
|
|
|
|
|
|
|
prices = defaultdict(lambda: None)
|
|
|
|
|
for line in lines:
|
|
|
|
|
default_uom = line.product.default_uom
|
|
|
|
|
if not uom or default_uom.category != uom.category:
|
|
|
|
|
product_uom = default_uom
|
|
|
|
|
else:
|
|
|
|
|
product_uom = uom
|
|
|
|
|
unit_price = UoM.compute_price(
|
|
|
|
|
line.unit, line.unit_price, product_uom)
|
|
|
|
|
if currency and line.purchase.currency != currency:
|
|
|
|
|
with Transaction().set_context(date=date):
|
|
|
|
|
unit_price = Currency.compute(
|
|
|
|
|
line.purchase.currency, unit_price, currency,
|
|
|
|
|
round=False)
|
|
|
|
|
prices[line.product.id] = round_price(unit_price)
|
|
|
|
|
return prices
|
|
|
|
|
|
2019-03-15 11:30:21 +01:00
|
|
|
def product_suppliers_used(self, **pattern):
|
2019-10-24 19:08:07 +02:00
|
|
|
# Skip rules to test pattern on all records
|
|
|
|
|
with Transaction().set_user(0):
|
|
|
|
|
product = self.__class__(self)
|
|
|
|
|
for product_supplier in product.product_suppliers:
|
2019-03-15 11:30:21 +01:00
|
|
|
if product_supplier.match(pattern):
|
|
|
|
|
yield product_supplier
|
|
|
|
|
pattern['product'] = None
|
|
|
|
|
yield from self.template.product_suppliers_used(**pattern)
|
|
|
|
|
|
2023-09-24 16:49:52 +02:00
|
|
|
def _get_purchase_unit_price(self, quantity=0):
|
|
|
|
|
return
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
@classmethod
|
|
|
|
|
def get_purchase_price(cls, products, quantity=0):
|
|
|
|
|
'''
|
|
|
|
|
Return purchase price for product ids.
|
|
|
|
|
The context that can have as keys:
|
|
|
|
|
uom: the unit of measure
|
|
|
|
|
supplier: the supplier party id
|
|
|
|
|
currency: the currency id for the returned price
|
|
|
|
|
'''
|
|
|
|
|
pool = Pool()
|
|
|
|
|
Uom = pool.get('product.uom')
|
2023-03-19 19:57:30 +01:00
|
|
|
Company = pool.get('company.company')
|
2013-09-23 11:53:12 +02:00
|
|
|
Currency = pool.get('currency.currency')
|
|
|
|
|
Date = pool.get('ir.date')
|
2014-05-09 23:18:23 +02:00
|
|
|
ProductSupplier = pool.get('purchase.product_supplier')
|
|
|
|
|
ProductSupplierPrice = pool.get('purchase.product_supplier.price')
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2014-05-09 23:18:23 +02:00
|
|
|
context = Transaction().context
|
|
|
|
|
prices = {}
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2017-11-21 20:47:31 +01:00
|
|
|
assert len(products) == len(set(products)), "Duplicate products"
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
uom = None
|
2014-05-09 23:18:23 +02:00
|
|
|
if context.get('uom'):
|
|
|
|
|
uom = Uom(context['uom'])
|
2013-09-23 11:53:12 +02:00
|
|
|
|
|
|
|
|
currency = None
|
2014-05-09 23:18:23 +02:00
|
|
|
if context.get('currency'):
|
|
|
|
|
currency = Currency(context['currency'])
|
2023-03-19 19:57:30 +01:00
|
|
|
elif context.get('company'):
|
|
|
|
|
currency = Company(context['company']).currency
|
2023-08-17 16:13:40 +02:00
|
|
|
date = context.get('purchase_date') or Date.today()
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2023-08-17 16:26:09 +02:00
|
|
|
last_purchase_prices = cls.get_last_purchase_price_uom(products)
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
for product in products:
|
2023-09-24 16:49:52 +02:00
|
|
|
unit_price = product._get_purchase_unit_price(quantity=quantity)
|
2013-09-23 11:53:12 +02:00
|
|
|
default_uom = product.default_uom
|
2023-03-19 19:57:30 +01:00
|
|
|
default_currency = currency
|
2017-04-02 12:52:15 +02:00
|
|
|
if not uom or default_uom.category != uom.category:
|
|
|
|
|
product_uom = default_uom
|
|
|
|
|
else:
|
|
|
|
|
product_uom = uom
|
2014-05-09 23:18:23 +02:00
|
|
|
pattern = ProductSupplier.get_pattern()
|
2019-03-15 11:30:21 +01:00
|
|
|
product_suppliers = product.product_suppliers_used(**pattern)
|
|
|
|
|
try:
|
|
|
|
|
product_supplier = next(product_suppliers)
|
|
|
|
|
except StopIteration:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
pattern = ProductSupplierPrice.get_pattern()
|
|
|
|
|
for price in product_supplier.prices:
|
|
|
|
|
if price.match(quantity, product_uom, pattern):
|
2021-09-13 23:29:56 +02:00
|
|
|
unit_price = price.unit_price
|
2023-08-21 09:30:21 +02:00
|
|
|
default_uom = product_supplier.unit
|
2019-03-15 11:30:21 +01:00
|
|
|
default_currency = product_supplier.currency
|
2023-08-17 16:26:09 +02:00
|
|
|
if unit_price is not None:
|
|
|
|
|
unit_price = Uom.compute_price(
|
|
|
|
|
default_uom, unit_price, product_uom)
|
|
|
|
|
if currency and default_currency:
|
|
|
|
|
with Transaction().set_context(date=date):
|
|
|
|
|
unit_price = Currency.compute(
|
|
|
|
|
default_currency, unit_price, currency,
|
|
|
|
|
round=False)
|
|
|
|
|
if unit_price is None:
|
|
|
|
|
unit_price = last_purchase_prices[product.id]
|
2023-09-24 16:49:52 +02:00
|
|
|
else:
|
|
|
|
|
unit_price = round_price(unit_price)
|
2021-09-13 23:29:56 +02:00
|
|
|
prices[product.id] = unit_price
|
2014-05-09 23:18:23 +02:00
|
|
|
return prices
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2020-05-01 20:31:02 +02:00
|
|
|
@classmethod
|
|
|
|
|
def copy(cls, products, default=None):
|
|
|
|
|
pool = Pool()
|
|
|
|
|
ProductSupplier = pool.get('purchase.product_supplier')
|
|
|
|
|
if default is None:
|
|
|
|
|
default = {}
|
|
|
|
|
else:
|
|
|
|
|
default = default.copy()
|
|
|
|
|
|
|
|
|
|
copy_suppliers = 'product_suppliers' not in default
|
|
|
|
|
if 'template' in default:
|
|
|
|
|
default.setdefault('product_suppliers', None)
|
|
|
|
|
new_products = super().copy(products, default)
|
|
|
|
|
if 'template' in default and copy_suppliers:
|
|
|
|
|
template2new = {}
|
|
|
|
|
product2new = {}
|
|
|
|
|
to_copy = []
|
|
|
|
|
for product, new_product in zip(products, new_products):
|
|
|
|
|
if product.product_suppliers:
|
|
|
|
|
to_copy.extend(product.product_suppliers)
|
|
|
|
|
template2new[product.template.id] = new_product.template.id
|
|
|
|
|
product2new[product.id] = new_product.id
|
|
|
|
|
if to_copy:
|
|
|
|
|
ProductSupplier.copy(to_copy, {
|
|
|
|
|
'product': lambda d: product2new[d['product']],
|
|
|
|
|
'template': lambda d: template2new[d['template']],
|
|
|
|
|
})
|
|
|
|
|
return new_products
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2021-01-17 01:24:33 +01:00
|
|
|
class ProductSupplier(
|
|
|
|
|
sequence_ordered(), DeactivableMixin, ModelSQL, ModelView, MatchMixin):
|
2013-09-23 11:53:12 +02:00
|
|
|
'Product Supplier'
|
|
|
|
|
__name__ = 'purchase.product_supplier'
|
2019-03-15 11:30:21 +01:00
|
|
|
template = fields.Many2One(
|
|
|
|
|
'product.template', "Product",
|
2022-10-11 00:44:50 +02:00
|
|
|
required=True, ondelete='CASCADE',
|
2019-03-15 11:30:21 +01:00
|
|
|
domain=[
|
|
|
|
|
If(Bool(Eval('product')),
|
|
|
|
|
('products', '=', Eval('product')),
|
|
|
|
|
()),
|
2021-01-17 01:24:33 +01:00
|
|
|
If(Eval('active'), ('active', '=', True), ()),
|
2019-03-15 11:30:21 +01:00
|
|
|
],
|
2023-10-26 22:53:18 +02:00
|
|
|
states={
|
|
|
|
|
'readonly': Eval('id', -1) >= 0,
|
|
|
|
|
},
|
2021-03-05 15:49:19 +01:00
|
|
|
context={
|
|
|
|
|
'company': Eval('company', -1),
|
|
|
|
|
},
|
2022-04-08 19:07:13 +02:00
|
|
|
depends={'company'})
|
2019-03-15 11:30:21 +01:00
|
|
|
product = fields.Many2One(
|
2022-10-11 00:44:50 +02:00
|
|
|
'product.product', "Variant",
|
2019-03-15 11:30:21 +01:00
|
|
|
domain=[
|
|
|
|
|
If(Bool(Eval('template')),
|
|
|
|
|
('template', '=', Eval('template')),
|
|
|
|
|
()),
|
2021-01-17 01:24:33 +01:00
|
|
|
If(Eval('active'), ('active', '=', True), ()),
|
2019-03-15 11:30:21 +01:00
|
|
|
],
|
2023-10-26 22:53:18 +02:00
|
|
|
states={
|
|
|
|
|
'readonly': Eval('id', -1) >= 0,
|
|
|
|
|
},
|
2021-03-05 15:49:19 +01:00
|
|
|
context={
|
|
|
|
|
'company': Eval('company', -1),
|
|
|
|
|
},
|
2022-04-08 19:07:13 +02:00
|
|
|
depends={'company'})
|
2021-03-05 15:49:19 +01:00
|
|
|
party = fields.Many2One(
|
|
|
|
|
'party.party', 'Supplier', required=True, ondelete='CASCADE',
|
2023-10-26 22:53:18 +02:00
|
|
|
states={
|
|
|
|
|
'readonly': Eval('id', -1) >= 0,
|
|
|
|
|
},
|
2021-03-05 15:49:19 +01:00
|
|
|
context={
|
|
|
|
|
'company': Eval('company', -1),
|
|
|
|
|
},
|
2022-04-08 19:07:13 +02:00
|
|
|
depends={'company'})
|
2022-10-11 00:44:50 +02:00
|
|
|
name = fields.Char("Name", translate=True)
|
|
|
|
|
code = fields.Char("Code")
|
2023-01-08 18:12:49 +01:00
|
|
|
prices = fields.One2Many(
|
|
|
|
|
'purchase.product_supplier.price', 'product_supplier', "Prices",
|
|
|
|
|
help="Add price for different criteria.\n"
|
|
|
|
|
"The last matching line is used.")
|
2021-04-20 09:18:58 +02:00
|
|
|
company = fields.Many2One(
|
|
|
|
|
'company.company', "Company",
|
2022-10-11 00:44:50 +02:00
|
|
|
required=True, ondelete='CASCADE')
|
2022-11-24 19:16:51 +01:00
|
|
|
lead_time = fields.TimeDelta(
|
|
|
|
|
"Lead Time",
|
|
|
|
|
domain=['OR',
|
|
|
|
|
('lead_time', '=', None),
|
|
|
|
|
('lead_time', '>=', TimeDelta()),
|
|
|
|
|
],
|
2021-01-19 14:59:54 +01:00
|
|
|
help="The time from confirming the purchase order to receiving the "
|
|
|
|
|
"products.\n"
|
|
|
|
|
"If empty the lead time of the supplier is used.")
|
2013-09-23 11:53:12 +02:00
|
|
|
currency = fields.Many2One('currency.currency', 'Currency', required=True,
|
|
|
|
|
ondelete='RESTRICT')
|
2023-08-21 09:30:21 +02:00
|
|
|
unit = fields.Function(
|
|
|
|
|
fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2022-10-11 00:44:50 +02:00
|
|
|
@classmethod
|
|
|
|
|
def __setup__(cls):
|
|
|
|
|
cls.code.search_unaccented = False
|
|
|
|
|
super().__setup__()
|
|
|
|
|
t = cls.__table__()
|
|
|
|
|
cls._sql_indexes.update({
|
|
|
|
|
Index(t, (t.code, Index.Similarity())),
|
|
|
|
|
})
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
@classmethod
|
|
|
|
|
def __register__(cls, module_name):
|
2018-07-24 22:01:08 +02:00
|
|
|
table = cls.__table_handler__(module_name)
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2019-03-15 11:30:21 +01:00
|
|
|
# Migration from 5.0: add product/template
|
|
|
|
|
if (table.column_exist('product')
|
|
|
|
|
and not table.column_exist('template')):
|
|
|
|
|
table.column_rename('product', 'template')
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
super(ProductSupplier, cls).__register__(module_name)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def default_company():
|
|
|
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def default_currency():
|
|
|
|
|
Company = Pool().get('company.company')
|
|
|
|
|
if Transaction().context.get('company'):
|
|
|
|
|
company = Company(Transaction().context['company'])
|
|
|
|
|
return company.currency.id
|
|
|
|
|
|
2019-10-16 21:20:29 +02:00
|
|
|
@fields.depends(
|
|
|
|
|
'product', '_parent_product.template')
|
2019-03-15 11:30:21 +01:00
|
|
|
def on_change_product(self):
|
|
|
|
|
if self.product:
|
|
|
|
|
self.template = self.product.template
|
|
|
|
|
|
2014-01-26 14:29:28 +01:00
|
|
|
@fields.depends('party')
|
2013-09-23 11:53:12 +02:00
|
|
|
def on_change_party(self):
|
2016-03-03 21:55:21 +01:00
|
|
|
cursor = Transaction().connection.cursor()
|
2014-10-29 20:40:09 +01:00
|
|
|
self.currency = self.default_currency()
|
2013-09-23 11:53:12 +02:00
|
|
|
if self.party:
|
|
|
|
|
table = self.__table__()
|
|
|
|
|
cursor.execute(*table.select(table.currency,
|
|
|
|
|
where=table.party == self.party.id,
|
|
|
|
|
group_by=table.currency,
|
|
|
|
|
order_by=Count(Literal(1)).desc))
|
|
|
|
|
row = cursor.fetchone()
|
|
|
|
|
if row:
|
2014-10-29 20:40:09 +01:00
|
|
|
self.currency, = row
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2014-05-27 22:24:21 +02:00
|
|
|
def get_rec_name(self, name):
|
2022-06-21 23:40:00 +02:00
|
|
|
if not self.name and not self.code:
|
|
|
|
|
if self.product:
|
|
|
|
|
name = self.product.rec_name
|
|
|
|
|
else:
|
|
|
|
|
name = self.template.rec_name
|
2017-12-18 17:56:39 +01:00
|
|
|
else:
|
2022-06-21 23:40:00 +02:00
|
|
|
if self.name:
|
|
|
|
|
name = self.name
|
|
|
|
|
elif self.product:
|
|
|
|
|
name = self.product.name
|
|
|
|
|
else:
|
|
|
|
|
name = self.template.name
|
|
|
|
|
if self.code:
|
|
|
|
|
name = '[' + self.code + ']' + name
|
2019-03-15 11:30:21 +01:00
|
|
|
return name
|
2014-05-27 22:24:21 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def search_rec_name(cls, name, clause):
|
2022-09-16 23:37:38 +02:00
|
|
|
_, operator, operand, *extra = clause
|
|
|
|
|
if operator.startswith('!') or operator.startswith('not '):
|
2015-04-05 23:38:20 +02:00
|
|
|
bool_op = 'AND'
|
|
|
|
|
else:
|
|
|
|
|
bool_op = 'OR'
|
2022-09-16 23:37:38 +02:00
|
|
|
code_value = operand
|
|
|
|
|
if operator.endswith('like') and is_full_text(operand):
|
|
|
|
|
code_value = lstrip_wildcard(operand)
|
2017-12-18 17:56:39 +01:00
|
|
|
domain = [bool_op,
|
2022-09-16 23:37:38 +02:00
|
|
|
('template', operator, operand, *extra),
|
|
|
|
|
('product', operator, operand, *extra),
|
|
|
|
|
('party', operator, operand, *extra),
|
|
|
|
|
('code', operator, code_value, *extra),
|
|
|
|
|
('name', operator, operand, *extra),
|
2014-05-27 22:24:21 +02:00
|
|
|
]
|
2017-12-18 17:56:39 +01:00
|
|
|
return domain
|
2014-05-27 22:24:21 +02:00
|
|
|
|
2020-10-09 19:09:05 +02:00
|
|
|
@fields.depends(
|
|
|
|
|
'product', '_parent_product.purchase_uom',
|
|
|
|
|
'template', '_parent_template.purchase_uom')
|
2023-08-21 09:30:21 +02:00
|
|
|
def on_change_with_unit(self, name=None):
|
2019-03-15 11:30:21 +01:00
|
|
|
if self.product:
|
2023-03-26 09:49:00 +02:00
|
|
|
return self.product.purchase_uom
|
2020-10-09 19:09:05 +02:00
|
|
|
elif self.template:
|
2023-03-26 09:49:00 +02:00
|
|
|
return self.template.purchase_uom
|
2014-05-09 23:18:23 +02:00
|
|
|
|
2021-01-19 14:59:54 +01:00
|
|
|
@property
|
|
|
|
|
def lead_time_used(self):
|
|
|
|
|
# Use getattr because it can be called with an unsaved instance
|
|
|
|
|
lead_time = getattr(self, 'lead_time', None)
|
|
|
|
|
party = getattr(self, 'party', None)
|
|
|
|
|
if lead_time is None and party:
|
2021-04-03 14:27:10 +02:00
|
|
|
company = getattr(self, 'company', None)
|
|
|
|
|
lead_time = party.get_multivalue(
|
|
|
|
|
'supplier_lead_time', company=company.id if company else None)
|
2021-01-19 14:59:54 +01:00
|
|
|
return lead_time
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
def compute_supply_date(self, date=None):
|
|
|
|
|
'''
|
|
|
|
|
Compute the supply date for the Product Supplier at the given date
|
|
|
|
|
'''
|
|
|
|
|
Date = Pool().get('ir.date')
|
|
|
|
|
|
|
|
|
|
if not date:
|
2022-01-23 12:54:56 +01:00
|
|
|
with Transaction().set_context(company=self.company.id):
|
|
|
|
|
date = Date.today()
|
2021-01-19 14:59:54 +01:00
|
|
|
if self.lead_time_used is None:
|
2013-09-23 11:53:12 +02:00
|
|
|
return datetime.date.max
|
2021-01-19 14:59:54 +01:00
|
|
|
return date + self.lead_time_used
|
2013-09-23 11:53:12 +02:00
|
|
|
|
|
|
|
|
def compute_purchase_date(self, date):
|
|
|
|
|
'''
|
|
|
|
|
Compute the purchase date for the Product Supplier at the given date
|
|
|
|
|
'''
|
|
|
|
|
Date = Pool().get('ir.date')
|
|
|
|
|
|
2021-02-12 23:58:33 +01:00
|
|
|
if self.lead_time_used is None or date is None:
|
2022-01-23 12:54:56 +01:00
|
|
|
with Transaction().set_context(company=self.company.id):
|
|
|
|
|
return Date.today()
|
2021-01-19 14:59:54 +01:00
|
|
|
return date - self.lead_time_used
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2014-05-09 23:18:23 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def get_pattern():
|
|
|
|
|
context = Transaction().context
|
2019-03-15 11:30:21 +01:00
|
|
|
pattern = {'party': context.get('supplier')}
|
|
|
|
|
if 'product_supplier' in context:
|
|
|
|
|
pattern['id'] = context['product_supplier']
|
|
|
|
|
return pattern
|
2013-09-23 11:53:12 +02:00
|
|
|
|
2014-05-09 23:18:23 +02:00
|
|
|
|
2016-09-13 14:08:49 +02:00
|
|
|
class ProductSupplierPrice(
|
|
|
|
|
sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
2013-09-23 11:53:12 +02:00
|
|
|
'Product Supplier Price'
|
|
|
|
|
__name__ = 'purchase.product_supplier.price'
|
|
|
|
|
product_supplier = fields.Many2One('purchase.product_supplier',
|
|
|
|
|
'Supplier', required=True, ondelete='CASCADE')
|
2018-02-28 18:58:09 +01:00
|
|
|
quantity = fields.Float(
|
|
|
|
|
"Quantity",
|
|
|
|
|
required=True,
|
|
|
|
|
domain=[('quantity', '>=', 0)],
|
2019-09-30 09:39:27 +02:00
|
|
|
help='Minimal quantity.')
|
2021-08-21 09:09:56 +02:00
|
|
|
unit_price = Monetary(
|
|
|
|
|
"Unit Price", currency='currency', required=True, digits=price_digits)
|
2014-05-09 23:18:23 +02:00
|
|
|
|
2023-08-21 09:30:21 +02:00
|
|
|
unit = fields.Function(fields.Many2One(
|
|
|
|
|
'product.uom', "Unit",
|
|
|
|
|
help="The unit in which the quantity is specified."),
|
|
|
|
|
'on_change_with_unit')
|
2020-10-09 19:09:05 +02:00
|
|
|
currency = fields.Function(
|
|
|
|
|
fields.Many2One('currency.currency', 'Currency'),
|
|
|
|
|
'on_change_with_currency')
|
|
|
|
|
|
2021-03-07 11:13:53 +01:00
|
|
|
@classmethod
|
|
|
|
|
def __setup__(cls):
|
|
|
|
|
super().__setup__()
|
|
|
|
|
cls.__access__.add('product_supplier')
|
|
|
|
|
|
2013-09-23 11:53:12 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def default_quantity():
|
|
|
|
|
return 0.0
|
2014-05-09 23:18:23 +02:00
|
|
|
|
2023-10-22 12:37:04 +02:00
|
|
|
@fields.depends('product_supplier', '_parent_product_supplier.unit')
|
2023-08-21 09:30:21 +02:00
|
|
|
def on_change_with_unit(self, name=None):
|
|
|
|
|
return self.product_supplier.unit if self.product_supplier else None
|
2020-10-09 19:09:05 +02:00
|
|
|
|
|
|
|
|
@fields.depends('product_supplier', '_parent_product_supplier.currency')
|
|
|
|
|
def on_change_with_currency(self, name=None):
|
2023-03-26 09:49:00 +02:00
|
|
|
if self.product_supplier:
|
|
|
|
|
return self.product_supplier.currency
|
2020-10-09 19:09:05 +02:00
|
|
|
|
2014-05-09 23:18:23 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def get_pattern():
|
|
|
|
|
return {}
|
|
|
|
|
|
2023-08-21 09:30:21 +02:00
|
|
|
def match(self, quantity, unit, pattern):
|
2014-05-09 23:18:23 +02:00
|
|
|
pool = Pool()
|
|
|
|
|
Uom = pool.get('product.uom')
|
|
|
|
|
test_quantity = Uom.compute_qty(
|
2023-08-21 09:30:21 +02:00
|
|
|
self.product_supplier.unit, self.quantity, unit)
|
2018-02-28 18:58:09 +01:00
|
|
|
if test_quantity > abs(quantity):
|
2014-05-09 23:18:23 +02:00
|
|
|
return False
|
|
|
|
|
return super(ProductSupplierPrice, self).match(pattern)
|