1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/industry_fsm_stock/wizard/fsm_stock_tracking.py
2024-12-10 09:04:09 +07:00

213 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError
class FsmStockTracking(models.TransientModel):
_name = 'fsm.stock.tracking'
_description = 'Track Stock'
task_id = fields.Many2one('project.task')
product_id = fields.Many2one('product.product')
tracking = fields.Selection(related='product_id.tracking')
tracking_line_ids = fields.One2many('fsm.stock.tracking.line', 'wizard_tracking_line')
tracking_validated_line_ids = fields.One2many('fsm.stock.tracking.line', 'wizard_tracking_line_validated')
company_id = fields.Many2one('res.company', 'Company')
is_same_warehouse = fields.Boolean('Same warehouse', compute="_compute_is_same_warehouse")
@api.depends('tracking_line_ids.is_same_warehouse')
def _compute_is_same_warehouse(self):
for tracking in self:
tracking.is_same_warehouse = all(tracking.tracking_line_ids.mapped('is_same_warehouse'))
def _update_lot_id(self, sale_line, tracking_line):
for move in sale_line.move_ids:
for ml in move.move_line_ids:
if ml.lot_id == sale_line.fsm_lot_id:
ml.lot_id = tracking_line.lot_id
def _get_moves_dict(self, sale_order):
default_warehouse = self.env.user._get_default_warehouse_id()
pickings_to_update = sale_order.picking_ids.filtered(
lambda p:
(
p.location_dest_id == default_warehouse.wh_pack_stock_loc_id
or p.location_dest_id == default_warehouse.wh_output_stock_loc_id
)
and p.state not in ['done', 'cancel']
)
move_read_group = self.env['stock.move']._read_group([
('picking_id', 'in', pickings_to_update.ids),
('product_id', '=', self.product_id.id)
], ['picking_id'], ['id:recordset'])
moves_per_picking = {}
for picking, moves in move_read_group:
moves_per_picking[picking] = moves
return moves_per_picking
def _remove_qty_from_intermediate_delivery(self, moves_from_intermediate_pickings, lot, qty_removed, deleted_line=False):
for pick in moves_from_intermediate_pickings:
qty_done_diff = qty_removed
for move in moves_from_intermediate_pickings[pick]:
if move.state in ['done, cancel']:
continue
for move_line in move.move_line_ids:
if move_line.quantity > 0:
if not move_line.lot_id:
move_line.lot_id = lot
elif move_line.lot_id != lot:
continue
new_line_qty = max(0, move_line.quantity + qty_done_diff)
qty_done_diff += move_line.quantity - new_line_qty
previous_qty = move_line.quantity
move_line.quantity = new_line_qty
if deleted_line and move.warehouse_id != self.task_id.sale_order_id.warehouse_id:
move.product_uom_qty -= previous_qty - new_line_qty
if qty_done_diff == 0:
break
if deleted_line and move.product_uom_qty == 0 and move.warehouse_id != self.task_id.sale_order_id.warehouse_id:
move.move_line_ids.unlink()
move.state = 'cancel'
if qty_done_diff == 0:
break
def _add_qty_to_intermediate_delivery_batch(self, moves_from_intermediate_pickings, lot, qty_added):
ml_to_create = []
for pick in moves_from_intermediate_pickings:
for move in moves_from_intermediate_pickings[pick]:
if move.state in ['done, cancel']:
continue
new_line_needed = True
for ml in move.move_line_ids:
if not ml.lot_id or ml.lot_id == lot:
if ml.quantity != qty_added:
ml.quantity = qty_added
new_line_needed = False
break
# if no ml were available, create a new one
if new_line_needed:
ml_vals = move._prepare_move_line_vals(quantity=0)
ml_vals['quantity'] = qty_added
ml_vals['lot_id'] = lot.id
ml_to_create.append(ml_vals)
return ml_to_create
def _add_qty_to_intermediate_delivery(self, moves_from_intermediate_pickings, line, qty_added):
self.env['stock.move.line'].create(self._add_qty_to_intermediate_delivery_batch(moves_from_intermediate_pickings, line.lot_id, qty_added))
def generate_lot(self):
self.ensure_one()
if self.tracking_line_ids.filtered(lambda l: not l.lot_id):
raise UserError(_('Each line needs a Lot/Serial Number'))
SaleOrderLine = self.env['sale.order.line'].sudo()
sale_lines_remove = SaleOrderLine.search([
('order_id', '=', self.task_id.sale_order_id.id),
('product_id', '=', self.product_id.id),
('id', 'not in', self.tracking_line_ids.sale_order_line_id.ids),
('task_id', '=', self.task_id.id)
])
# create the new sale_lines from the wizard
move_line_qty_per_lot_id = defaultdict(int)
new_lines = self.tracking_line_ids.filtered(lambda line: not line.sale_order_line_id)
for line in new_lines:
qty = line.quantity if self.tracking == 'lot' else 1
vals = {
'order_id': self.task_id.sale_order_id.id,
'product_id': self.product_id.id,
'product_uom_qty': qty,
'task_id': self.task_id.id,
'fsm_lot_id': line.lot_id.id,
}
move_line_qty_per_lot_id[line.lot_id] += qty
SaleOrderLine.with_context(industry_fsm_stock_tracking=True).create(vals)
dict_moves_per_picking = self._get_moves_dict(self.task_id.sale_order_id)
if dict_moves_per_picking: # create/update the move_lines for the intermediate deliveries
ml_to_create = []
for lot_id, qty in move_line_qty_per_lot_id.items():
ml_to_create.extend(self._add_qty_to_intermediate_delivery_batch(dict_moves_per_picking, lot_id, qty))
self.env['stock.move.line'].create(ml_to_create)
# set the qty to 0 for the sol and the deliveries from the deleted wizard line
for sl in sale_lines_remove:
if sl.qty_delivered == 0 and sl.fsm_lot_id:
if dict_moves_per_picking: # handles 2/3 ways deliveries
self._remove_qty_from_intermediate_delivery(dict_moves_per_picking, sl.fsm_lot_id, 0-(sl.product_uom_qty - sl.qty_delivered), deleted_line=True)
editable_moves = sl.move_ids.filtered(lambda m: m.state not in ['done', 'cancel'])
editable_moves.move_line_ids.unlink()
editable_moves.lot_ids -= sl.fsm_lot_id
sl.fsm_lot_id = False
# If the warehouse of the delivery is different from the one of the sale_order, the procurement.group.run() will not handle the moves correctly.
# We have to manually set the product_uom_qty and cancel the moves
if sl.order_id.warehouse_id != self.env.user._get_default_warehouse_id():
editable_moves.product_uom_qty = 0
editable_moves.state = 'cancel'
sl.product_uom_qty = sl.qty_delivered
# update the sol and deliveries according to the modification of the wizard lines
for line in self.tracking_line_ids:
qty = line.quantity if self.tracking == 'lot' else 1
sol = line.sale_order_line_id
if sol:
if dict_moves_per_picking: # handles 2/3 ways deliveries
if sol.fsm_lot_id == line.lot_id:
current_qty_demand = sol.product_uom_qty - sol.qty_delivered
if qty > current_qty_demand:
self._add_qty_to_intermediate_delivery(dict_moves_per_picking, line, qty)
elif qty < current_qty_demand:
self._remove_qty_from_intermediate_delivery(dict_moves_per_picking, sol.fsm_lot_id, qty - current_qty_demand)
else: # the sn/lot id was changed
# adds the qty of the new lot_id
self._add_qty_to_intermediate_delivery(dict_moves_per_picking, line, qty)
# removes the qty of the old lot_id
self._remove_qty_from_intermediate_delivery(dict_moves_per_picking, sol.fsm_lot_id, 0-(sol.product_uom_qty - sol.qty_delivered))
if sol.fsm_lot_id != line.lot_id: # update the lot_id of the deliveries linked to the sale_order_line
self._update_lot_id(sol, line)
line.sale_order_line_id.with_context(industry_fsm_stock_tracking=True).write(
{
'fsm_lot_id': line.lot_id,
'product_uom_qty': qty + line.sale_order_line_id.qty_delivered,
})
class FsmStockTrackingLine(models.TransientModel):
_name = 'fsm.stock.tracking.line'
_description = 'Lines for FSM Stock Tracking'
def _default_warehouse_id(self):
return [Command.set([self.env.user._get_default_warehouse_id().id])]
lot_id = fields.Many2one('stock.lot', string='Lot/Serial Number', domain="[('product_id', '=', product_id)]", check_company=True)
quantity = fields.Float(required=True, default=1)
product_id = fields.Many2one('product.product')
sale_order_line_id = fields.Many2one('sale.order.line')
company_id = fields.Many2one('res.company', 'Company')
wizard_tracking_line = fields.Many2one('fsm.stock.tracking', string="Tracking Line")
wizard_tracking_line_validated = fields.Many2one('fsm.stock.tracking', string="Validated Tracking Line")
is_same_warehouse = fields.Boolean('Same warehouse', compute="_compute_warehouse", default=True)
warehouse_id = fields.Many2one("stock.warehouse", compute="_compute_warehouse", default=_default_warehouse_id)
@api.depends_context('uid')
def _compute_warehouse(self):
default_warehouse = self.env.user._get_default_warehouse_id()
for line in self:
if not isinstance(line.id, models.NewId):
so_lines_warehouses = line.sale_order_line_id.move_ids.warehouse_id
if len(so_lines_warehouses) > 1:
# If there is more than one we ensure not taking the default warehouse in order to avoid unwanted
# moves as this would mean we already have several move_ids (which would only occur through the use
# of other apps than FSM (inventory app for instance)). This should indeed not be possible through
# the use of the FSM app only.
line.warehouse_id = so_lines_warehouses.filtered(lambda w: w.id != default_warehouse.id)[0]
else:
line.warehouse_id = so_lines_warehouses or default_warehouse
else:
line.warehouse_id = default_warehouse
line.is_same_warehouse = not line.warehouse_id or (line.warehouse_id == default_warehouse)