Files
tryton/modules/sale_stock_quantity/sale.py
T

261 lines
10 KiB
Python
Raw Normal View History

2015-09-02 10:18:04 +02: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.
2018-06-26 19:11:54 +02:00
2015-09-02 10:18:04 +02:00
import datetime
2021-10-30 02:27:19 +02:00
from collections import defaultdict
from itertools import groupby
from operator import attrgetter
2015-09-02 10:18:04 +02:00
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2022-04-11 23:41:01 +02:00
from trytond.model import ModelView, Workflow, fields
2021-12-11 17:58:44 +01:00
from trytond.pool import Pool, PoolMeta
2026-03-22 21:05:26 +01:00
from trytond.tools import sortable_values
2021-12-11 17:58:44 +01:00
from trytond.transaction import Transaction
2015-09-02 10:18:04 +02:00
2018-12-29 14:20:29 +01:00
from .exceptions import StockQuantityError, StockQuantityWarning
2015-09-02 10:18:04 +02:00
2018-06-26 19:11:54 +02:00
class Sale(metaclass=PoolMeta):
2015-09-02 10:18:04 +02:00
__name__ = 'sale.sale'
@classmethod
@ModelView.button
@Workflow.transition('quotation')
def quote(cls, sales):
2020-01-21 00:33:11 +01:00
# Check before setting the number
2015-09-02 10:18:04 +02:00
cls._check_stock_quantity(sales)
2025-04-04 00:45:18 +02:00
super().quote(sales)
2015-09-02 10:18:04 +02:00
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, sales):
2020-01-21 00:33:11 +01:00
# Check before queueing the process task
2015-09-02 10:18:04 +02:00
cls._check_stock_quantity(sales)
2025-04-04 00:45:18 +02:00
super().confirm(sales)
2015-09-02 10:18:04 +02:00
@classmethod
def _check_stock_quantity(cls, sales):
for sale in sales:
sale.check_stock_quantity()
@classmethod
def _stock_quantity_states(cls):
return ['quotation', 'confirmed']
@classmethod
def _stock_quantity_next_supply_date(cls, product):
pool = Pool()
try:
PurchaseRequest = pool.get('purchase.request')
except KeyError:
PurchaseRequest = None
if (getattr(PurchaseRequest, 'get_supply_dates', None)
and product.purchasable):
2015-09-02 10:18:04 +02:00
return PurchaseRequest.get_supply_dates(product)[0]
else:
# TODO compute supply date for production
return datetime.date.max
def check_stock_quantity(self):
pool = Pool()
Product = pool.get('product.product')
Date = pool.get('ir.date')
Uom = pool.get('product.uom')
Group = pool.get('res.group')
User = pool.get('res.user')
ModelData = pool.get('ir.model.data')
Lang = pool.get('ir.lang')
2015-09-02 10:18:04 +02:00
Line = pool.get('sale.line')
2018-12-29 14:20:29 +01:00
Warning = pool.get('res.user.warning')
2015-09-02 10:18:04 +02:00
if not self.warehouse:
return
2015-09-02 10:18:04 +02:00
transaction = Transaction()
def in_group():
group = Group(ModelData.get_id('sale_stock_quantity',
'group_sale_stock_quantity'))
user_id = transaction.user
if user_id == 0:
user_id = transaction.context.get('user', user_id)
if user_id == 0:
return True
user = User(user_id)
return group in user.groups
def filter_line(line):
return (line.warehouse
and line.product
2015-09-02 10:18:04 +02:00
and line.product.type == 'goods'
2017-05-30 23:50:42 +02:00
and not line.product.consumable
2015-10-21 18:28:07 +02:00
and line.quantity > 0
# Use getattr as supply_on_sale comes from sale_supply module
and not getattr(line, 'supply_on_sale', False))
2015-09-02 10:18:04 +02:00
def get_delta(date, warehouse, products):
2015-09-02 10:18:04 +02:00
'Compute quantity delta at the date'
if date in date2delta:
return date2delta[warehouse][date]
2015-09-02 10:18:04 +02:00
with transaction.set_context(forecast=True,
stock_date_start=date,
stock_date_end=date):
2021-10-30 02:27:19 +02:00
pbl = defaultdict(int)
2026-03-22 21:05:26 +01:00
product_ids = [p.id for p in products]
pbl.update(Product.products_by_location(
[warehouse.id],
with_childs=True,
grouping_filter=(product_ids,)))
2015-09-02 10:18:04 +02:00
delta = {}
2018-06-26 19:11:54 +02:00
for key, qty in pbl.items():
2015-09-02 10:18:04 +02:00
_, product_id = key
delta[product_id] = qty
date2delta[warehouse][date] = delta
2015-09-02 10:18:04 +02:00
return delta
date2delta = defaultdict(dict)
2015-09-02 10:18:04 +02:00
def raise_(line_id, message_values):
if not in_group():
2018-12-29 14:20:29 +01:00
raise StockQuantityError(
gettext('sale_stock_quantity.msg_sale_stock_quantity',
**message_values))
2015-09-02 10:18:04 +02:00
warning_name = 'stock_quantity_warning_%s' % line_id
2018-12-29 14:20:29 +01:00
if Warning.check(warning_name):
raise StockQuantityWarning(warning_name,
gettext('sale_stock_quantity.msg_sale_stock_quantity',
**message_values))
2015-09-02 10:18:04 +02:00
with Transaction().set_context(company=self.company.id):
today = Date.today()
lang = Lang.get()
2015-09-02 10:18:04 +02:00
lines = filter(filter_line, self.lines)
lines = list(lines)
2015-09-02 10:18:04 +02:00
quantities = defaultdict(lambda: defaultdict(int))
w_getter = attrgetter('warehouse', 'shipping_date')
w_products = {}
for (warehouse, shipping_date), w_lines in groupby(
sorted(lines, key=sortable_values(w_getter)), key=w_getter):
if shipping_date is None:
shipping_date = today
w_products[warehouse] = products = {l.product for l in w_lines}
with transaction.set_context(
locations=[warehouse.id],
stock_date_end=shipping_date,
stock_assign=True):
products = Product.browse(products)
quantities[warehouse].update(
(p, p.forecast_quantity) for p in products)
2015-09-02 10:18:04 +02:00
# Remove quantities from other sales
2026-03-22 21:05:26 +01:00
other_lines = Line.search([
('sale.company', '=', self.company.id),
('sale.state', 'in', self._stock_quantity_states()),
('sale.id', '!=', self.id),
('product', 'in', products),
('quantity', '>', 0),
])
for line in other_lines:
if line.warehouse != warehouse:
continue
if (line.shipping_date
and line.shipping_date > shipping_date):
continue
product = line.product
date = line.sale.sale_date or today
if date > today:
continue
quantity = Uom.compute_qty(line.unit, line.quantity,
product.default_uom, round=False)
quantities[line.warehouse][product] -= quantity
for line in lines:
warehouse = line.warehouse
2015-09-02 10:18:04 +02:00
product = line.product
quantity = Uom.compute_qty(line.unit, line.quantity,
product.default_uom, round=False)
shipping_date = line.shipping_date or today
2015-09-02 10:18:04 +02:00
next_supply_date = self._stock_quantity_next_supply_date(product)
message_values = {
'line': line.rec_name,
'forecast_quantity': lang.format_number_symbol(
quantities[warehouse][product],
product.default_uom, product.default_uom.digits),
'quantity': lang.format_number_symbol(
line.quantity, line.unit, line.unit.digits),
2015-09-02 10:18:04 +02:00
}
if (quantities[warehouse][product] < quantity
and shipping_date < next_supply_date):
2015-09-02 10:18:04 +02:00
raise_(line.id, message_values)
# Update quantities if the same product is many times in lines
quantities[warehouse][product] -= quantity
2015-09-02 10:18:04 +02:00
# Check other dates until next supply date
if next_supply_date != datetime.date.max:
products = w_products[warehouse]
forecast_quantity = quantities[warehouse][product]
date = shipping_date + datetime.timedelta(1)
2015-09-02 10:18:04 +02:00
while date < next_supply_date:
delta = get_delta(date, warehouse, products)
2015-09-02 10:18:04 +02:00
forecast_quantity += delta.get(product.id, 0)
if forecast_quantity < 0:
message_values['forecast_quantity'] = forecast_quantity
raise_(line.id, message_values)
date += datetime.timedelta(1)
2022-04-11 23:41:01 +02:00
class Line(metaclass=PoolMeta):
__name__ = 'sale.line'
@fields.depends(methods=['_notify_stock_quantity'])
def on_change_notify(self):
notifications = super().on_change_notify()
notifications.extend(self._notify_stock_quantity())
return notifications
@fields.depends(
'sale_state', 'product', 'quantity', 'unit', 'company',
'shipping_date', 'sale', '_parent_sale.warehouse')
2022-04-11 23:41:01 +02:00
def _notify_stock_quantity(self):
pool = Pool()
Date = pool.get('ir.date')
Lang = pool.get('ir.lang')
Move = pool.get('stock.move')
Product = pool.get('product.product')
UoM = pool.get('product.uom')
2022-04-11 23:41:01 +02:00
lang = Lang.get()
if (self.sale_state == 'draft'
and self.sale
and self.sale.warehouse
2022-04-11 23:41:01 +02:00
and self.product
and self.product.type in Move.get_product_types()
and self.unit
and self.quantity is not None):
with Transaction().set_context(
company=self.company.id if self.company else None):
today = Date.today()
shipping_date = self.shipping_date or today
locations = [self.sale.warehouse.id]
2022-04-11 23:41:01 +02:00
with Transaction().set_context(
locations=locations,
stock_date_end=shipping_date):
2022-04-11 23:41:01 +02:00
product = Product(self.product.id)
quantity = UoM.compute_qty(
self.unit, self.quantity,
product.default_uom, round=False)
if product.forecast_quantity < quantity:
2022-04-11 23:41:01 +02:00
yield ('warning', gettext(
'sale_stock_quantity'
'.msg_product_forecast_quantity_lower',
forecast_quantity=lang.format_number_symbol(
product.forecast_quantity, product.default_uom,
product.default_uom.digits),
2022-04-11 23:41:01 +02:00
product=self.product.rec_name,
quantity=lang.format_number_symbol(
self.quantity, self.unit,
self.unit.digits)))