Files
tradon/modules/sale_supply/sale.py
2026-03-14 09:42:12 +00:00

286 lines
10 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.
from collections import defaultdict
from itertools import groupby
from operator import attrgetter
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
def is_done(self):
done = super().is_done()
if done:
if any(l.supply_state == 'requested'
for l in self.lines if l.supply_on_sale):
return False
return done
@classmethod
def _process_shipment(cls, sales):
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)
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):
product_quantities.update(
(p, p.forecast_quantity)
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_shipment(sales)
@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:
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)
def create_purchase_requests(self, product_quantities):
requests, lines = [], []
for line in self.lines:
request = line.get_purchase_request(product_quantities)
if not request:
continue
requests.append(request)
assert not line.purchase_request
line.purchase_request = request
lines.append(line)
return requests, lines
@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)])
class Line(metaclass=PoolMeta):
__name__ = 'sale.line'
purchase_request = fields.Many2One('purchase.request', 'Purchase Request',
ondelete='SET NULL', readonly=True)
supply_state = fields.Function(fields.Selection([
('', ""),
('requested', "Requested"),
('supplied', "Supplied"),
('cancelled', "Cancelled"),
], "Supply State",
states={
'invisible': ~Eval('supply_state'),
}), 'get_supply_state')
@property
def has_supply(self):
return bool(self.purchase_request)
def get_supply_state(self, name):
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'
return 'requested'
return ''
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('purchase_request', None)
return super().copy(lines, default=default)
@property
def supply_on_sale(self):
"Returns True if the sale line has to be supply by purchase request"
if (self.type != 'line'
or self.sale.shipment_method == 'manual'
or not self.product
or self.quantity <= 0
or any(m.state not in ['staging', 'cancelled']
for m in self.moves)):
return False
return bool(self.product.supply_on_sale)
@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
def get_move(self, shipment_type):
move = super().get_move(shipment_type)
if (move
and shipment_type == 'out'
and self.has_supply):
if self.supply_state == 'requested':
move.state = 'staging'
return move
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,
}
def get_purchase_request(self, product_quantities):
"""Return purchase request for the sale line
depending on the product quantities"""
pool = Pool()
Uom = pool.get('product.uom')
Request = pool.get('purchase.request')
if (not self.supply_on_sale
or self.purchase_request
or not self.ready_for_supply
or not self.product.purchasable):
return
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
supplier, purchase_date = Request.find_best_supplier(product,
self.shipping_date,
**self._get_purchase_request_product_supplier_pattern())
unit = product.purchase_uom or product.default_uom
quantity = Uom.compute_qty(self.unit, quantity, unit)
return Request(
product=product,
party=supplier,
quantity=quantity,
unit=unit,
computed_quantity=quantity,
computed_unit=unit,
purchase_date=purchase_date,
supply_date=self.shipping_date,
company=self.sale.company,
warehouse=self.warehouse,
origin=self.sale,
)
def assign_supplied(self, quantities, grouping=('product',)):
'''
Assign supplied move
The quantities will be updated according to assigned quantities.
'''
pool = Pool()
Move = pool.get('stock.move')
ShipmentOut = pool.get('stock.shipment.out')
if self.supply_state != 'supplied':
return
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,
})