first commit
This commit is contained in:
285
modules/sale_supply/sale.py
Normal file
285
modules/sale_supply/sale.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# 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,
|
||||
})
|
||||
Reference in New Issue
Block a user