Files

286 lines
10 KiB
Python
Raw Permalink Normal View History

2014-12-02 12:56:01 +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.
2023-02-26 10:41:40 +01:00
from collections import defaultdict
from itertools import groupby
from operator import attrgetter
from trytond.model import fields
2013-02-26 20:50:05 +01:00
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
2013-02-26 20:50:05 +01:00
2018-06-26 19:11:55 +02:00
class Sale(metaclass=PoolMeta):
2013-02-26 20:50:05 +01:00
__name__ = 'sale.sale'
2014-01-31 22:22:52 +01:00
def is_done(self):
2025-04-04 00:45:18 +02:00
done = super().is_done()
2014-01-31 22:22:52 +01:00
if done:
if any(l.supply_state == 'requested'
for l in self.lines if l.supply_on_sale):
2014-01-31 22:22:52 +01:00
return False
return done
@classmethod
def _process_fulfillment(cls, sales):
2023-02-26 10:41:40 +01:00
pool = Pool()
Product = pool.get('product.product')
product_quantities = defaultdict(float)
for (company, warehouse), sub_sales in groupby(
filter(attrgetter('warehouse'), sales),
key=attrgetter('company', 'warehouse')):
sub_sales = list(sub_sales)
2023-02-26 10:41:40 +01:00
products = {
l.product for s in sub_sales for l in s.lines
if l.product and l.product.supply_on_sale == 'stock_first'}
locations = [warehouse.id]
with Transaction().set_context(
company=company.id, locations=locations,
stock_assign=True,
stock_date_end=None):
2023-02-26 10:41:40 +01:00
product_quantities.update(
(p, p.forecast_quantity)
2023-02-26 10:41:40 +01:00
for p in Product.browse(products))
# purchase requests must be created before shipments to get
# information about requests during the shipments creation like the
# supplier
cls._process_supply(sub_sales, product_quantities)
product_quantities.clear()
super()._process_fulfillment(sales)
2023-02-26 10:41:40 +01:00
@classmethod
def _process_supply(cls, sales, product_quantities):
pool = Pool()
Line = pool.get('sale.line')
PurchaseRequest = pool.get('purchase.request')
requests, lines = [], []
for sale in sales:
2023-02-26 10:41:40 +01:00
reqs, lns = sale.create_purchase_requests(product_quantities)
requests.extend(reqs)
lines.extend(lns)
PurchaseRequest.save(requests)
Line.save(lines)
cls._update_moves_from_supply(sales)
2013-02-26 20:50:05 +01:00
2023-02-26 10:41:40 +01:00
def create_purchase_requests(self, product_quantities):
requests, lines = [], []
2013-02-26 20:50:05 +01:00
for line in self.lines:
2023-02-26 10:41:40 +01:00
request = line.get_purchase_request(product_quantities)
2013-02-26 20:50:05 +01:00
if not request:
continue
2020-04-13 14:49:07 +02:00
requests.append(request)
2013-02-26 20:50:05 +01:00
assert not line.purchase_request
line.purchase_request = request
2020-04-13 14:49:07 +02:00
lines.append(line)
return requests, lines
2013-02-26 20:50:05 +01:00
@classmethod
def _update_moves_from_supply(cls, sales):
pool = Pool()
Shipment = pool.get('stock.shipment.out')
Move = pool.get('stock.move')
shipments = set()
def add_shipment(shipment):
if (isinstance(shipment, Shipment)
and shipment.state in {'draft', 'waiting'}):
shipments.add(shipment)
moves_to_draft, moves_to_cancel = [], []
for sale in sales:
for line in sale.lines:
for move in line.moves:
if move.state == 'staging':
if (not line.has_supply
or line.supply_state == 'supplied'):
moves_to_draft.append(move)
elif line.supply_state == 'cancelled':
moves_to_cancel.append(move)
else:
continue
add_shipment(move.shipment)
if moves_to_draft:
Move.draft(moves_to_draft)
shipments = Shipment.browse(shipments)
shipments_waiting = [s for s in shipments if s.state == 'waiting']
if shipments:
Shipment.draft(shipments) # clear inventory moves
if moves_to_cancel:
Move.cancel(moves_to_cancel)
if shipments:
Shipment.wait([
s for s in shipments_waiting
if any(m.state != 'cancelled' for m in s.moves)])
Shipment.cancel([
s for s in shipments
if all(m.state == 'cancelled' for m in s.moves)])
2013-02-26 20:50:05 +01:00
2020-03-01 12:33:54 +01:00
class Line(metaclass=PoolMeta):
2013-02-26 20:50:05 +01:00
__name__ = 'sale.line'
purchase_request = fields.Many2One('purchase.request', 'Purchase Request',
ondelete='SET NULL', readonly=True)
2020-04-13 14:49:07 +02:00
supply_state = fields.Function(fields.Selection([
('', ""),
('requested', "Requested"),
('supplied', "Supplied"),
('cancelled', "Cancelled"),
], "Supply State",
2013-02-26 20:50:05 +01:00
states={
2020-04-13 14:49:07 +02:00
'invisible': ~Eval('supply_state'),
}), 'get_supply_state')
2013-02-26 20:50:05 +01:00
2020-04-13 14:49:07 +02:00
@property
def has_supply(self):
return bool(self.purchase_request)
def get_supply_state(self, name):
2014-09-17 21:45:56 +02:00
if self.purchase_request is not None:
if self.purchase_request.state == 'cancelled':
return 'cancelled'
else:
purchase_line = self.purchase_request.purchase_line
if purchase_line is not None:
purchase = purchase_line.purchase
if purchase.state in {'processing', 'done'}:
return 'supplied'
2014-09-17 21:45:56 +02:00
return 'requested'
return ''
2013-02-26 20:50:05 +01:00
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
2018-09-01 01:02:04 +02:00
else:
default = default.copy()
default.setdefault('purchase_request', None)
2020-03-01 12:33:54 +01:00
return super().copy(lines, default=default)
2013-02-26 20:50:05 +01:00
@property
def supply_on_sale(self):
"Returns True if the sale line has to be supply by purchase request"
if (self.type != 'line'
2021-03-27 21:06:59 +01:00
or self.sale.shipment_method == 'manual'
2013-02-26 20:50:05 +01:00
or not self.product
or self.quantity <= 0
2020-06-06 10:30:11 +01:00
or any(m.state not in ['staging', 'cancelled']
2020-03-01 12:33:54 +01:00
for m in self.moves)):
2013-02-26 20:50:05 +01:00
return False
2022-12-21 23:52:51 +01:00
return bool(self.product.supply_on_sale)
2013-02-26 20:50:05 +01:00
2023-02-26 10:41:40 +01:00
@property
def ready_for_supply(self):
if self.sale.shipment_method == 'invoice':
# Ensure to create the request for the maximum paid
invoice_skips = (
set(self.sale.invoices_ignored)
| set(self.sale.invoices_recreated))
invoice_lines = [
l for l in self.invoice_lines
if l.invoice not in invoice_skips]
if (not invoice_lines
or any(
(not l.invoice) or l.invoice.state != 'paid'
for l in invoice_lines)):
return False
return True
2013-02-26 20:50:05 +01:00
def get_move(self, shipment_type):
2020-03-01 12:33:54 +01:00
move = super().get_move(shipment_type)
if (move
and shipment_type == 'out'
2023-02-26 10:41:40 +01:00
and self.has_supply):
if self.supply_state == 'requested':
move.state = 'staging'
2013-02-26 20:50:05 +01:00
return move
2023-02-26 10:41:40 +01:00
def _get_move_quantity(self, shipment_type):
quantity = super()._get_move_quantity(shipment_type)
if self.supply_on_sale and not self.ready_for_supply:
quantity = 0
return quantity
def _get_purchase_request_product_supplier_pattern(self):
return {
'company': self.sale.company.id,
}
2023-02-26 10:41:40 +01:00
def get_purchase_request(self, product_quantities):
"""Return purchase request for the sale line
depending on the product quantities"""
2013-02-26 20:50:05 +01:00
pool = Pool()
Uom = pool.get('product.uom')
Request = pool.get('purchase.request')
2020-04-13 14:49:07 +02:00
if (not self.supply_on_sale
or self.purchase_request
2023-02-26 10:41:40 +01:00
or not self.ready_for_supply
2020-04-13 14:49:07 +02:00
or not self.product.purchasable):
2013-02-26 20:50:05 +01:00
return
2023-02-26 10:41:40 +01:00
product = self.product
quantity = self._get_move_quantity('out')
if product.supply_on_sale == 'stock_first':
available_qty = product_quantities[product]
available_qty = Uom.compute_qty(
product.default_uom, available_qty, self.unit,
round=False)
if available_qty > 0:
product_quantities[product] -= Uom.compute_qty(
self.unit, quantity, product.default_uom, round=False)
return
2013-02-26 20:50:05 +01:00
supplier, purchase_date = Request.find_best_supplier(product,
self.shipping_date,
**self._get_purchase_request_product_supplier_pattern())
2023-08-21 09:31:27 +02:00
unit = product.purchase_uom or product.default_uom
quantity = Uom.compute_qty(self.unit, quantity, unit)
2014-09-17 21:45:56 +02:00
return Request(
product=product,
party=supplier,
quantity=quantity,
2023-08-21 09:31:27 +02:00
unit=unit,
2014-09-17 21:45:56 +02:00
computed_quantity=quantity,
2023-08-21 09:31:27 +02:00
computed_unit=unit,
2014-09-17 21:45:56 +02:00
purchase_date=purchase_date,
2016-02-02 21:16:47 +01:00
supply_date=self.shipping_date,
2014-09-17 21:45:56 +02:00
company=self.sale.company,
warehouse=self.warehouse,
origin=self.sale,
)
2013-02-26 20:50:05 +01:00
def assign_supplied(self, quantities, grouping=('product',)):
2013-02-26 20:50:05 +01:00
'''
Assign supplied move
The quantities will be updated according to assigned quantities.
2013-02-26 20:50:05 +01:00
'''
pool = Pool()
Move = pool.get('stock.move')
ShipmentOut = pool.get('stock.shipment.out')
2013-02-26 20:50:05 +01:00
2020-04-13 14:49:07 +02:00
if self.supply_state != 'supplied':
2013-02-26 20:50:05 +01:00
return
2013-02-26 20:50:05 +01:00
moves = set()
for move in self.moves:
shipment = move.shipment
if isinstance(shipment, ShipmentOut):
if shipment.warehouse_storage == shipment.warehouse_output:
inventory_moves = shipment.outgoing_moves
else:
inventory_moves = shipment.inventory_moves
for inv_move in inventory_moves:
if inv_move.product == self.product:
moves.add(inv_move)
Move.assign_try(list(moves), grouping=grouping, pblc={
self.company.id: quantities,
})