Merge pull request #223 from ForgeFlow/15.0-fp-211-and-219

[15.0][FWPORT] rma and rma_sale: serial/lot numbers improvements
This commit is contained in:
Lois Rilo
2022-02-10 13:59:04 +01:00
committed by GitHub
7 changed files with 245 additions and 12 deletions

View File

@@ -595,7 +595,19 @@ class RmaOrderLine(models.Model):
self.reference_move_id = False
return True
def _check_production_lot_assigned(self):
for rec in self:
if rec.product_id.tracking == "serial" and rec.product_qty != 1:
raise ValidationError(
_(
"Product %s has serial tracking configuration, "
"quantity to receive should be 1"
)
% (rec.product_id.display_name)
)
def action_rma_to_approve(self):
self._check_production_lot_assigned()
self.write({"state": "to_approve"})
for rec in self:
if rec.product_id.rma_approval_policy == "one_step":

View File

@@ -60,3 +60,9 @@ class StockMove(models.Model):
return self.move_dest_ids[0]._get_last_usage()
else:
return self.location_dest_id.usage
def _should_bypass_reservation(self, forced_location=False):
res = super()._should_bypass_reservation(forced_location=forced_location)
if self.env.context.get("force_no_bypass_reservation"):
return False
return res

View File

@@ -36,6 +36,43 @@ class RmaAddStockMove(models.TransientModel):
string="Stock Moves",
domain="[('state', '=', 'done')]",
)
show_lot_filter = fields.Boolean(
string="Show lot filter?",
compute="_compute_lot_domain",
)
lot_domain_ids = fields.Many2many(
comodel_name="stock.production.lot",
string="Lots Domain",
compute="_compute_lot_domain",
)
@api.depends(
"move_ids.move_line_ids.lot_id",
)
def _compute_lot_domain(self):
for rec in self:
rec.lot_domain_ids = rec.mapped("move_ids.move_line_ids.lot_id").ids
rec.show_lot_filter = bool(rec.lot_domain_ids)
lot_ids = fields.Many2many(
comodel_name="stock.production.lot", string="Lots/Serials selected"
)
def select_all(self):
self.ensure_one()
self.write(
{
"lot_ids": [(6, 0, self.lot_domain_ids.ids)],
}
)
return {
"type": "ir.actions.act_window",
"name": _("Add from Stock Move"),
"view_mode": "form",
"res_model": self._name,
"res_id": self.id,
"target": "new",
}
def _prepare_rma_line_from_stock_move(self, sm, lot=False):
if self.env.context.get("customer"):
@@ -74,6 +111,15 @@ class RmaAddStockMove(models.TransientModel):
raise ValidationError(
_("Please define a warehouse with a default RMA location")
)
product_qty = sm.product_uom_qty
if sm.product_id.tracking == "serial":
product_qty = 1
elif sm.product_id.tracking == "lot":
product_qty = sum(
sm.move_line_ids.filtered(lambda x: x.lot_id.id == lot.id).mapped(
"qty_done"
)
)
data = {
"partner_id": self.partner_id.id,
"reference_move_id": sm.id,
@@ -82,7 +128,7 @@ class RmaAddStockMove(models.TransientModel):
"origin": sm.picking_id.name or sm.name,
"uom_id": sm.product_uom.id,
"operation_id": operation.id,
"product_qty": sm.product_uom_qty,
"product_qty": product_qty,
"delivery_address_id": sm.picking_id.partner_id.id,
"rma_id": self.rma_id.id,
"receipt_policy": operation.receipt_policy,
@@ -110,16 +156,21 @@ class RmaAddStockMove(models.TransientModel):
rma_line_obj = self.env["rma.order.line"]
existing_stock_moves = self._get_existing_stock_moves()
for sm in self.move_ids:
if sm not in existing_stock_moves:
tracking_move = sm.product_id.tracking in ("serial", "lot")
if sm not in existing_stock_moves or tracking_move:
if sm.product_id.tracking == "none":
data = self._prepare_rma_line_from_stock_move(sm, lot=False)
rma_line_obj.with_context(default_rma_id=self.rma_id.id).create(
data
)
else:
lot_ids = [x.lot_id.id for x in sm.move_line_ids if x.lot_id]
data = self._prepare_rma_line_from_stock_move(sm, lot=lot_ids[0])
rma_line_obj.with_context(default_rma_id=self.rma_id.id).create(
data
)
for lot in sm.move_line_ids.mapped("lot_id").filtered(
lambda x: x.id in self.lot_ids.ids
):
if lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids:
continue
data = self._prepare_rma_line_from_stock_move(sm, lot)
rma_line_obj.with_context(default_rma_id=self.rma_id.id).create(
data
)
return {"type": "ir.actions.act_window_close"}

View File

@@ -32,6 +32,35 @@
<field name="state" />
</tree>
</field>
<field name="show_lot_filter" invisible="1" />
<field name="lot_domain_ids" widget="many2many_tags" invisible="1" />
<div
class="oe_grey"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
>
The creation of the RMA Lines will be separated according to the lots or serials selected
</div>
<div class="o_row">
<label
for="lot_ids"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
string="Selected Lot/Serial Numbers"
/>
<field
name="lot_ids"
widget="many2many_tags"
domain="[('id', 'in', lot_domain_ids)]"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
options="{'no_create': True}"
/>
<button
name="select_all"
type="object"
string="Select all"
class="oe_inline"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
/>
</div>
<footer>
<button
string="Confirm"

View File

@@ -189,11 +189,55 @@ class RmaMakePicking(models.TransientModel):
def action_create_picking(self):
self._create_picking()
move_line_model = self.env["stock.move.line"]
picking_type = self.env.context.get("picking_type")
pickings = self.env["stock.picking"].browse()
if picking_type == "outgoing":
pickings = self.mapped("item_ids.line_id")._get_out_pickings()
action = self.item_ids.line_id.action_view_out_shipments()
else:
pickings = self.mapped("item_ids.line_id")._get_in_pickings()
action = self.item_ids.line_id.action_view_in_shipments()
for move in pickings.move_lines.filtered(
lambda x: x.state not in ("draft", "cancel", "done")
and x.rma_line_id
and x.product_id.tracking in ("lot", "serial")
and x.rma_line_id.lot_id
):
move.move_line_ids.unlink()
if move.product_id.tracking == "serial":
move.write(
{
"lot_ids": [(6, 0, move.rma_line_id.lot_id.ids)],
}
)
move.move_line_ids.write(
{
"product_uom_qty": 1,
"qty_done": 0,
}
)
elif move.product_id.tracking == "lot":
if picking_type == "incoming":
qty = self.item_ids.filtered(
lambda x: x.line_id.id == move.rma_line_id.id
).qty_to_receive
else:
qty = self.item_ids.filtered(
lambda x: x.line_id.id == move.rma_line_id.id
).qty_to_deliver
move_line_data = move._prepare_move_line_vals()
move_line_data.update(
{
"lot_id": move.rma_line_id.lot_id.id,
"product_uom_id": move.product_id.uom_id.id,
"qty_done": 0,
"product_uom_qty": qty,
}
)
move_line_model.create(move_line_data)
if picking_type == "incoming":
pickings.with_context(force_no_bypass_reservation=True).action_assign()
return action
def action_cancel(self):

View File

@@ -41,8 +41,50 @@ class RmaAddSale(models.TransientModel):
readonly=False,
string="Sale Lines",
)
show_lot_filter = fields.Boolean(
string="Show lot filter?",
compute="_compute_lot_domain",
)
lot_domain_ids = fields.Many2many(
comodel_name="stock.production.lot",
string="Lots Domain",
compute="_compute_lot_domain",
)
def _prepare_rma_line_from_sale_order_line(self, line):
@api.depends(
"sale_line_ids.move_ids.move_line_ids.lot_id",
)
def _compute_lot_domain(self):
for rec in self:
rec.lot_domain_ids = (
rec.mapped("sale_line_ids.move_ids")
.filtered(lambda x: x.state == "done")
.mapped("move_line_ids.lot_id")
.ids
)
rec.show_lot_filter = bool(rec.lot_domain_ids)
lot_ids = fields.Many2many(
comodel_name="stock.production.lot", string="Lots/Serials selected"
)
def select_all(self):
self.ensure_one()
self.write(
{
"lot_ids": [(6, 0, self.lot_domain_ids.ids)],
}
)
return {
"type": "ir.actions.act_window",
"name": _("Add Sale Order"),
"view_mode": "form",
"res_model": self._name,
"res_id": self.id,
"target": "new",
}
def _prepare_rma_line_from_sale_order_line(self, line, lot=None):
operation = line.product_id.rma_customer_operation_id
if not operation:
operation = line.product_id.categ_id.rma_customer_operation_id
@@ -70,14 +112,24 @@ class RmaAddSale(models.TransientModel):
raise ValidationError(
_("Please define a warehouse with a " "default rma location.")
)
product_qty = line.product_uom_qty
if line.product_id.tracking == "serial":
product_qty = 1
elif line.product_id.tracking == "lot":
product_qty = sum(
line.mapped("move_ids.move_line_ids")
.filtered(lambda x: x.lot_id.id == lot.id)
.mapped("qty_done")
)
data = {
"partner_id": self.partner_id.id,
"sale_line_id": line.id,
"product_id": line.product_id.id,
"lot_id": lot and lot.id or False,
"origin": line.order_id.name,
"uom_id": line.product_uom.id,
"operation_id": operation.id,
"product_qty": line.product_uom_qty,
"product_qty": product_qty,
"delivery_address_id": self.sale_id.partner_id.id,
"invoice_address_id": self.sale_id.partner_id.id,
"price_unit": line.currency_id._convert(
@@ -118,10 +170,20 @@ class RmaAddSale(models.TransientModel):
rma_line_obj = self.env["rma.order.line"]
existing_sale_lines = self._get_existing_sale_lines()
for line in self.sale_line_ids:
tracking_move = line.product_id.tracking in ("serial", "lot")
# Load a PO line only once
if line not in existing_sale_lines:
data = self._prepare_rma_line_from_sale_order_line(line)
rma_line_obj.create(data)
if line not in existing_sale_lines or tracking_move:
if not tracking_move:
data = self._prepare_rma_line_from_sale_order_line(line)
rma_line_obj.create(data)
else:
for lot in line.mapped("move_ids.move_line_ids.lot_id").filtered(
lambda x: x.id in self.lot_ids.ids
):
if lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids:
continue
data = self._prepare_rma_line_from_sale_order_line(line, lot)
rma_line_obj.create(data)
rma = self.rma_id
data_rma = self._get_rma_data()
rma.write(data_rma)

View File

@@ -42,6 +42,35 @@
</tree>
</field>
<field name="show_lot_filter" invisible="1" />
<field name="lot_domain_ids" widget="many2many_tags" invisible="1" />
<div
class="oe_grey"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
>
The creation of the RMA Lines will be separated according to the lots or serials selected
</div>
<div class="o_row">
<label
for="lot_ids"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
string="Selected Lot/Serial Numbers"
/>
<field
name="lot_ids"
widget="many2many_tags"
domain="[('id', 'in', lot_domain_ids)]"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
options="{'no_create': True}"
/>
<button
name="select_all"
type="object"
string="Select all"
class="oe_inline"
attrs="{'invisible': [('show_lot_filter', '=', False)]}"
/>
</div>
<footer>
<button
string="Confirm"