first commit
This commit is contained in:
236
modules/sale/stock.py
Normal file
236
modules/sale/stock.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# 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 decimal import Decimal
|
||||
from functools import wraps
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelView, Workflow, fields
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.product import round_price
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.transaction import Transaction, without_check_access
|
||||
|
||||
|
||||
def process_sale(moves_field):
|
||||
def _process_sale(func):
|
||||
@wraps(func)
|
||||
def wrapper(cls, shipments):
|
||||
pool = Pool()
|
||||
Sale = pool.get('sale.sale')
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
with without_check_access():
|
||||
sales = set(m.sale for s in cls.browse(shipments)
|
||||
for m in getattr(s, moves_field) if m.sale)
|
||||
result = func(cls, shipments)
|
||||
if sales:
|
||||
with transaction.set_context(
|
||||
queue_batch=context.get('queue_batch', True)):
|
||||
Sale.__queue__.process(sales)
|
||||
return result
|
||||
return wrapper
|
||||
return _process_sale
|
||||
|
||||
|
||||
class ShipmentOut(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.out'
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('draft')
|
||||
def draft(cls, shipments):
|
||||
SaleLine = Pool().get('sale.line')
|
||||
for shipment in shipments:
|
||||
for move in shipment.outgoing_moves:
|
||||
if (move.state == 'cancelled'
|
||||
and isinstance(move.origin, SaleLine)):
|
||||
raise AccessError(
|
||||
gettext('sale.msg_sale_move_reset_draft',
|
||||
move=move.rec_name))
|
||||
|
||||
return super().draft(shipments)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('shipped')
|
||||
@process_sale('outgoing_moves')
|
||||
def ship(cls, shipments):
|
||||
super().ship(shipments)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('done')
|
||||
@process_sale('outgoing_moves')
|
||||
def do(cls, shipments):
|
||||
super().do(shipments)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('cancelled')
|
||||
@process_sale('outgoing_moves')
|
||||
def cancel(cls, shipments):
|
||||
super().cancel(shipments)
|
||||
|
||||
|
||||
class ShipmentOutReturn(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.out.return'
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('draft')
|
||||
def draft(cls, shipments):
|
||||
SaleLine = Pool().get('sale.line')
|
||||
for shipment in shipments:
|
||||
for move in shipment.incoming_moves:
|
||||
if (move.state == 'cancelled'
|
||||
and isinstance(move.origin, SaleLine)):
|
||||
raise AccessError(
|
||||
gettext('sale.msg_sale_move_reset_draft',
|
||||
move=move.rec_name))
|
||||
|
||||
return super().draft(shipments)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('received')
|
||||
@process_sale('incoming_moves')
|
||||
def receive(cls, shipments):
|
||||
pool = Pool()
|
||||
SaleLine = pool.get('sale.line')
|
||||
for shipment in shipments:
|
||||
for move in shipment.incoming_moves:
|
||||
if isinstance(move.origin, SaleLine):
|
||||
move.origin.check_move_quantity()
|
||||
super().receive(shipments)
|
||||
|
||||
|
||||
def process_sale_move(func):
|
||||
@wraps(func)
|
||||
def wrapper(cls, moves):
|
||||
pool = Pool()
|
||||
Sale = pool.get('sale.sale')
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
with without_check_access():
|
||||
sales = set(m.sale for m in cls.browse(moves) if m.sale)
|
||||
result = func(cls, moves)
|
||||
if sales:
|
||||
with transaction.set_context(
|
||||
queue_batch=context.get('queue_batch', True)):
|
||||
Sale.__queue__.process(sales)
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
|
||||
class Move(metaclass=PoolMeta):
|
||||
__name__ = 'stock.move'
|
||||
sale = fields.Function(
|
||||
fields.Many2One('sale.sale', "Sale"),
|
||||
'get_sale', searcher='search_sale')
|
||||
sale_exception_state = fields.Function(fields.Selection([
|
||||
('', ''),
|
||||
('ignored', 'Ignored'),
|
||||
('recreated', 'Recreated'),
|
||||
], 'Exception State'), 'get_sale_exception_state')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
if not cls.origin.domain:
|
||||
cls.origin.domain = {}
|
||||
cls.origin.domain['sale.line'] = [
|
||||
('type', '=', 'line'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_origin(cls):
|
||||
models = super()._get_origin()
|
||||
models.append('sale.line')
|
||||
return models
|
||||
|
||||
@classmethod
|
||||
def check_origin_types(cls):
|
||||
types = super().check_origin_types()
|
||||
types.add('customer')
|
||||
return types
|
||||
|
||||
def get_sale(self, name):
|
||||
SaleLine = Pool().get('sale.line')
|
||||
if isinstance(self.origin, SaleLine):
|
||||
return self.origin.sale.id
|
||||
|
||||
@classmethod
|
||||
def search_sale(cls, name, clause):
|
||||
return [('origin.' + clause[0],) + tuple(clause[1:3])
|
||||
+ ('sale.line',) + tuple(clause[3:])]
|
||||
|
||||
def get_sale_exception_state(self, name):
|
||||
SaleLine = Pool().get('sale.line')
|
||||
if not isinstance(self.origin, SaleLine):
|
||||
return ''
|
||||
if self in self.origin.moves_recreated:
|
||||
return 'recreated'
|
||||
if self in self.origin.moves_ignored:
|
||||
return 'ignored'
|
||||
return ''
|
||||
|
||||
@fields.depends('origin')
|
||||
def on_change_with_product_uom_category(self, name=None):
|
||||
pool = Pool()
|
||||
SaleLine = pool.get('sale.line')
|
||||
category = super().on_change_with_product_uom_category(name=name)
|
||||
# Enforce the same unit category as they are used to compute the
|
||||
# remaining quantity to ship and the quantity to invoice.
|
||||
# Use getattr as reference field can have negative id
|
||||
if (isinstance(self.origin, SaleLine)
|
||||
and getattr(self.origin, 'unit', None)):
|
||||
category = self.origin.unit.category
|
||||
return category
|
||||
|
||||
def get_cost_price(self, product_cost_price=None):
|
||||
pool = Pool()
|
||||
SaleLine = pool.get('sale.line')
|
||||
Sale = pool.get('sale.sale')
|
||||
# For return sale's move use the cost price of the original sale
|
||||
if (isinstance(self.origin, SaleLine)
|
||||
and self.origin.quantity < 0
|
||||
and self.from_location.type != 'storage'
|
||||
and self.to_location.type == 'storage'
|
||||
and isinstance(self.origin.sale.origin, Sale)):
|
||||
sale = self.origin.sale.origin
|
||||
cost = Decimal(0)
|
||||
qty = Decimal(0)
|
||||
for move in sale.moves:
|
||||
if (move.state == 'done'
|
||||
and move.from_location.type == 'storage'
|
||||
and move.to_location.type == 'customer'
|
||||
and move.product == self.product):
|
||||
move_quantity = Decimal(str(move.internal_quantity))
|
||||
cost_price = move.get_cost_price(
|
||||
product_cost_price=move.cost_price)
|
||||
qty += move_quantity
|
||||
cost += cost_price * move_quantity
|
||||
if qty:
|
||||
product_cost_price = round_price(cost / qty)
|
||||
return super().get_cost_price(product_cost_price=product_cost_price)
|
||||
|
||||
@property
|
||||
def origin_name(self):
|
||||
pool = Pool()
|
||||
SaleLine = pool.get('sale.line')
|
||||
name = super().origin_name
|
||||
if isinstance(self.origin, SaleLine) and self.origin.id >= 0:
|
||||
name = self.origin.sale.rec_name
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('cancelled')
|
||||
@process_sale_move
|
||||
def cancel(cls, moves):
|
||||
super().cancel(moves)
|
||||
|
||||
@classmethod
|
||||
@process_sale_move
|
||||
def on_delete(cls, moves):
|
||||
return super().on_delete(moves)
|
||||
Reference in New Issue
Block a user