# Copyright (C) 2017-20 ForgeFlow S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) import operator from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError ops = {"=": operator.eq, "!=": operator.ne} class RmaOrderLine(models.Model): _name = "rma.order.line" _description = "RMA" _inherit = ["mail.thread"] _order = "id desc" @api.model def _get_default_type(self): if "supplier" in self.env.context: return "supplier" return "customer" @api.model def _default_warehouse_id(self): rma_id = self.env.context.get("default_rma_id", False) warehouse = self.env["stock.warehouse"] if rma_id: rma = self.env["rma.order"].browse(rma_id) warehouse = self.env["stock.warehouse"].search( [("company_id", "=", rma.company_id.id)], limit=1 ) return warehouse @api.model def _default_location_id(self): wh = self._default_warehouse_id() return wh.lot_rma_id @api.onchange("partner_id") def _onchange_delivery_address(self): self.delivery_address_id = self.env["res.partner"].browse( self.partner_id.address_get(["delivery"])["delivery"] ) @api.model def _get_in_pickings(self): # We consider an in move one where the first origin is outside # of the company and the final destination is outside. In case # of 2 or 3 step pickings, we should categorize as in shipments # even when they are technically internal transfers. pickings = self.env["stock.picking"] for move in self.move_ids: first_usage = move._get_first_usage() last_usage = move._get_last_usage() if last_usage == "internal" and first_usage != "internal": pickings |= move.picking_id elif last_usage == "supplier" and first_usage == "customer": pickings |= move.picking_id return pickings @api.model def _get_in_moves(self): moves = self.env["stock.move"] for move in self.move_ids: first_usage = move._get_first_usage() last_usage = move._get_last_usage() if last_usage == "internal" and first_usage != "internal": moves |= move elif last_usage == "supplier" and first_usage == "customer": moves |= move return moves @api.model def _get_out_pickings(self): pickings = self.env["stock.picking"] for move in self.move_ids: first_usage = move._get_first_usage() last_usage = move._get_last_usage() if first_usage in ("internal", "production") and last_usage in ( "customer", "supplier", ): pickings |= move.picking_id elif last_usage == "customer" and first_usage == "supplier": pickings |= move.picking_id return pickings def _compute_in_shipment_count(self): for line in self: pickings = line._get_in_pickings() line.in_shipment_count = len(pickings) def _compute_out_shipment_count(self): for line in self: pickings = line._get_out_pickings() line.out_shipment_count = len(pickings) def _get_rma_move_qty(self, states, direction="in"): for rec in self: product_obj = self.env["uom.uom"] qty = 0.0 if direction == "in": moves = rec.move_ids.filtered( lambda m: m.state in states and ( m.location_id.usage == "supplier" or m.location_id.usage == "customer" ) and ( m.location_dest_id.usage == "internal" or m.location_dest_id.usage == "supplier" ) ) elif direction == "out": moves = rec.move_ids.filtered( lambda m: m.state in states and ( m.location_dest_id.usage == "supplier" or m.location_dest_id.usage == "customer" ) and ( m.location_id.usage == "internal" or m.location_id.usage == "supplier" ) ) for move in moves: # If the move is part of a chain don't count it if direction == "out" and move.move_orig_ids: continue elif direction == "in" and move.move_dest_ids: continue qty += product_obj._compute_quantity(move.product_uom_qty, rec.uom_id) return qty @api.depends( "move_ids", "move_ids.state", "qty_received", "receipt_policy", "product_qty", "type", ) def _compute_qty_to_receive(self): for rec in self: rec.qty_to_receive = 0.0 if rec.receipt_policy == "ordered": rec.qty_to_receive = rec.product_qty - rec.qty_received elif rec.receipt_policy == "delivered": rec.qty_to_receive = rec.qty_delivered - rec.qty_received @api.depends( "move_ids", "move_ids.state", "delivery_policy", "product_qty", "type", "qty_delivered", "qty_received", ) def _compute_qty_to_deliver(self): for rec in self: rec.qty_to_deliver = 0.0 if rec.delivery_policy == "ordered": rec.qty_to_deliver = rec.product_qty - rec.qty_delivered elif rec.delivery_policy == "received": rec.qty_to_deliver = rec.qty_received - rec.qty_delivered @api.depends("move_ids", "move_ids.state", "type") def _compute_qty_incoming(self): for rec in self: qty = rec._get_rma_move_qty( ("draft", "confirmed", "assigned"), direction="in" ) rec.qty_incoming = qty @api.depends("move_ids", "move_ids.state", "type") def _compute_qty_received(self): for rec in self: qty = rec._get_rma_move_qty("done", direction="in") rec.qty_received = qty @api.depends("move_ids", "move_ids.state", "type") def _compute_qty_outgoing(self): for rec in self: qty = rec._get_rma_move_qty( ("draft", "confirmed", "assigned"), direction="out" ) rec.qty_outgoing = qty @api.depends("move_ids", "move_ids.state", "type") def _compute_qty_delivered(self): for rec in self: qty = rec._get_rma_move_qty("done", direction="out") rec.qty_delivered = qty @api.model def _get_supplier_rma_qty(self): return sum( self.supplier_rma_line_ids.filtered(lambda r: r.state != "cancel").mapped( "product_qty" ) ) @api.depends( "customer_to_supplier", "supplier_rma_line_ids", "move_ids", "move_ids.state", "qty_received", "receipt_policy", "product_qty", "type", ) def _compute_qty_supplier_rma(self): for rec in self: if rec.customer_to_supplier: supplier_rma_qty = rec._get_supplier_rma_qty() rec.qty_to_supplier_rma = rec.product_qty - supplier_rma_qty rec.qty_in_supplier_rma = supplier_rma_qty else: rec.qty_to_supplier_rma = 0.0 rec.qty_in_supplier_rma = 0.0 def _compute_rma_line_count(self): for rec in self.filtered(lambda r: r.type == "customer"): rec.rma_line_count = len(rec.supplier_rma_line_ids) for rec in self.filtered(lambda r: r.type == "supplier"): rec.rma_line_count = len(rec.customer_rma_id) @api.model def _default_date_rma(self): return fields.Datetime.now() delivery_address_id = fields.Many2one( comodel_name="res.partner", string="Partner delivery address", readonly=True, states={"draft": [("readonly", False)]}, help="This address will be used to deliver repaired or replacement " "products.", ) rma_id = fields.Many2one( comodel_name="rma.order", string="RMA Group", tracking=True, readonly=True, ) name = fields.Char( string="Reference", required=True, default="/", readonly=True, states={"draft": [("readonly", False)]}, help="Add here the supplier RMA #. Otherwise an internal code is" " assigned.", copy=False, ) description = fields.Text() conditions = fields.Html(string="Terms and conditions") origin = fields.Char( string="Source Document", readonly=True, states={"draft": [("readonly", False)]}, help="Reference of the document that produced this rma.", ) date_rma = fields.Datetime( string="Order Date", index=True, default=lambda self: self._default_date_rma() ) state = fields.Selection( selection=[ ("draft", "Draft"), ("to_approve", "To Approve"), ("approved", "Approved"), ("done", "Done"), ("canceled", "Canceled"), ], default="draft", tracking=True, ) operation_id = fields.Many2one( comodel_name="rma.operation", required=True, readonly=False, tracking=True, ) assigned_to = fields.Many2one( comodel_name="res.users", tracking=True, default=lambda self: self.env.uid, ) requested_by = fields.Many2one( comodel_name="res.users", tracking=True, default=lambda self: self.env.uid, ) partner_id = fields.Many2one( comodel_name="res.partner", required=True, store=True, tracking=True, readonly=True, states={"draft": [("readonly", False)]}, ) sequence = fields.Integer( default=10, help="Gives the sequence of this line when displaying the rma." ) product_id = fields.Many2one( comodel_name="product.product", ondelete="restrict", required=True, readonly=True, states={"draft": [("readonly", False)]}, ) product_tracking = fields.Selection(related="product_id.tracking") lot_id = fields.Many2one( comodel_name="stock.lot", string="Lot/Serial Number", readonly=True, states={"draft": [("readonly", False)]}, ) product_qty = fields.Float( string="Return Qty", copy=False, default=1.0, digits="Product Unit of Measure", readonly=True, states={"draft": [("readonly", False)]}, ) uom_id = fields.Many2one( comodel_name="uom.uom", string="Unit of Measure", required=True, readonly=True, compute="_compute_uom_id", precompute=True, store=True, states={"draft": [("readonly", False)]}, ) price_unit = fields.Monetary( string="Unit cost", readonly=True, states={"draft": [("readonly", False)]}, help="Unit cost of the items under RMA", ) in_shipment_count = fields.Integer( compute="_compute_in_shipment_count", string="# of Shipments" ) out_shipment_count = fields.Integer( compute="_compute_out_shipment_count", string="# of Deliveries" ) move_ids = fields.One2many( "stock.move", "rma_line_id", string="Stock Moves", readonly=True, copy=False ) reference_move_id = fields.Many2one( comodel_name="stock.move", string="Originating Stock Move", copy=False, readonly=True, states={"draft": [("readonly", False)]}, ) currency_id = fields.Many2one( "res.currency", default=lambda self: self.env.company.currency_id, ) company_id = fields.Many2one( comodel_name="res.company", required=True, default=lambda self: self.env.company, ) type = fields.Selection( selection=[("customer", "Customer"), ("supplier", "Supplier")], required=True, default=lambda self: self._get_default_type(), readonly=True, ) customer_to_supplier = fields.Boolean( "The customer will send to the supplier", readonly=True, states={"draft": [("readonly", False)]}, ) supplier_to_customer = fields.Boolean( "The supplier will send to the customer", readonly=True, states={"draft": [("readonly", False)]}, ) receipt_policy = fields.Selection( [ ("no", "Not required"), ("ordered", "Based on Ordered Quantities"), ("delivered", "Based on Delivered Quantities"), ], required=True, default="no", readonly=False, ) delivery_policy = fields.Selection( [ ("no", "Not required"), ("ordered", "Based on Ordered Quantities"), ("received", "Based on Received Quantities"), ], required=True, default="no", readonly=False, ondelete="cascade", ) in_route_id = fields.Many2one( "stock.route", string="Inbound Route", required=True, domain=[("rma_selectable", "=", True)], readonly=True, compute="_compute_in_route_id", precompute=True, store=True, states={"draft": [("readonly", False)]}, ) out_route_id = fields.Many2one( "stock.route", string="Outbound Route", required=True, domain=[("rma_selectable", "=", True)], readonly=True, compute="_compute_out_route_id", precompute=True, store=True, states={"draft": [("readonly", False)]}, ) in_warehouse_id = fields.Many2one( comodel_name="stock.warehouse", string="Inbound Warehouse", required=True, readonly=True, states={"draft": [("readonly", False)]}, default=lambda self: self._default_warehouse_id(), ) out_warehouse_id = fields.Many2one( comodel_name="stock.warehouse", string="Outbound Warehouse", required=True, readonly=True, states={"draft": [("readonly", False)]}, default=lambda self: self._default_warehouse_id(), ) location_id = fields.Many2one( comodel_name="stock.location", string="Send To This Company Location", required=True, readonly=True, states={"draft": [("readonly", False)]}, default=lambda self: self._default_location_id(), ) customer_rma_id = fields.Many2one( "rma.order.line", string="Customer RMA line", ondelete="cascade" ) supplier_rma_line_ids = fields.One2many("rma.order.line", "customer_rma_id") rma_line_count = fields.Integer( compute="_compute_rma_line_count", string="# of RMA lines associated" ) supplier_address_id = fields.Many2one( comodel_name="res.partner", readonly=True, states={"draft": [("readonly", False)]}, string="Supplier Address", help="Address of the supplier in case of Customer RMA operation " "dropship.", ) customer_address_id = fields.Many2one( comodel_name="res.partner", readonly=True, states={"draft": [("readonly", False)]}, string="Customer Address", help="Address of the customer in case of Supplier RMA operation " "dropship.", ) qty_to_receive = fields.Float( digits="Product Unit of Measure", compute="_compute_qty_to_receive", store=True, ) qty_incoming = fields.Float( string="Incoming Qty", copy=False, readonly=True, digits="Product Unit of Measure", compute="_compute_qty_incoming", store=True, ) qty_received = fields.Float( copy=False, digits="Product Unit of Measure", compute="_compute_qty_received", store=True, ) qty_to_deliver = fields.Float( copy=False, digits="Product Unit of Measure", readonly=True, compute="_compute_qty_to_deliver", store=True, ) qty_outgoing = fields.Float( string="Outgoing Qty", copy=False, readonly=True, digits="Product Unit of Measure", compute="_compute_qty_outgoing", store=True, ) qty_delivered = fields.Float( copy=False, digits="Product Unit of Measure", readonly=True, compute="_compute_qty_delivered", store=True, ) qty_to_supplier_rma = fields.Float( string="Qty to send to Supplier RMA", digits="Product Unit of Measure", readonly=True, compute="_compute_qty_supplier_rma", store=True, ) qty_in_supplier_rma = fields.Float( string="Qty in Supplier RMA", digits="Product Unit of Measure", readonly=True, compute="_compute_qty_supplier_rma", store=True, ) under_warranty = fields.Boolean( string="Under Warranty?", readonly=True, states={"draft": [("readonly", False)]} ) def _prepare_rma_line_from_stock_move(self, sm, lot=False): if not self.type: self.type = self._get_default_type() if self.type == "customer": operation = ( sm.product_id.rma_customer_operation_id or sm.product_id.categ_id.rma_customer_operation_id ) else: operation = ( sm.product_id.rma_supplier_operation_id or sm.product_id.categ_id.rma_supplier_operation_id ) if not operation: operation = self.env["rma.operation"].search( [("type", "=", self.type)], limit=1 ) if not operation: raise ValidationError(_("Please define an operation first.")) if not operation.in_route_id or not operation.out_route_id: route = self.env["stock.route"].search( [("rma_selectable", "=", True)], limit=1 ) if not route: raise ValidationError(_("Please define an RMA route.")) if ( not operation.in_warehouse_id or not operation.out_warehouse_id or not ( operation.in_warehouse_id.lot_rma_id or operation.out_warehouse_id.lot_rma_id ) ): warehouse = self.env["stock.warehouse"].search( [("company_id", "=", self.company_id.id), ("lot_rma_id", "!=", False)], limit=1, ) if not warehouse: raise ValidationError( _("Please define a warehouse with a default RMA location.") ) data = { "product_id": sm.product_id.id, "lot_id": lot and lot.id or False, "origin": sm.picking_id.name or sm.name, "uom_id": sm.product_uom.id, "product_qty": sm.product_uom_qty, "delivery_address_id": sm.picking_id.partner_id.id, "operation_id": operation.id, "receipt_policy": operation.receipt_policy, "delivery_policy": operation.delivery_policy, "in_warehouse_id": operation.in_warehouse_id.id or warehouse.id, "out_warehouse_id": operation.out_warehouse_id.id or warehouse.id, "in_route_id": operation.in_route_id.id or route.id, "out_route_id": operation.out_route_id.id or route.id, "location_id": ( operation.location_id.id or operation.in_warehouse_id.lot_rma_id.id or operation.out_warehouse_id.lot_rma_id.id or warehouse.lot_rma_id.id ), } return data @api.onchange("reference_move_id") def _onchange_reference_move_id(self): self.ensure_one() sm = self.reference_move_id if not sm: return if sm.move_line_ids.lot_id: if len(sm.move_line_ids.lot_id) > 1: raise UserError(_("To manage lots use RMA groups.")) else: data = self._prepare_rma_line_from_stock_move( sm, lot=sm.move_line_ids.lot_id[0] ) self.update(data) else: data = self._prepare_rma_line_from_stock_move(sm, lot=False) self.update(data) self._remove_other_data_origin("reference_move_id") @api.constrains("reference_move_id", "partner_id") def _check_move_partner(self): for rec in self: if ( rec.reference_move_id and rec.reference_move_id.picking_id.partner_id != rec.partner_id ): raise ValidationError( _( "RMA customer and originating stock move customer " "doesn't match." ) ) def _remove_other_data_origin(self, exception): if not exception == "reference_move_id": self.reference_move_id = False return True def _check_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_lot_assigned() self.write({"state": "to_approve"}) for rec in self: if rec.product_id.rma_approval_policy == "one_step": rec.action_rma_approve() return True def action_rma_draft(self): self.write({"state": "draft"}) return True def action_rma_approve(self): self.write({"state": "approved"}) return True def action_rma_done(self): self.write({"state": "done"}) return True @api.model_create_multi def create(self, vals_list): for vals in vals_list: if not vals.get("name") or vals.get("name") == "/": if self.env.context.get("supplier"): vals["name"] = self.env["ir.sequence"].next_by_code( "rma.order.line.supplier" ) else: vals["name"] = self.env["ir.sequence"].next_by_code( "rma.order.line.customer" ) return super().create(vals_list) def check_cancel(self): for move in self.move_ids: if move.state == "done": raise UserError( _("Unable to cancel %s as some receptions have already been done.") % (self.name) ) def action_rma_cancel(self): for order in self: order.check_cancel() # cancel ongoing orig moves # dest move cancelation can be managed with propagate_cancel option # on stock rules. moves = order.move_ids to_cancel_orig_moves = self.env["stock.move"] while moves: moves = moves.move_orig_ids.filtered( lambda m: m.state not in ("done", "cancel") and m.picking_id ) to_cancel_orig_moves |= moves to_cancel_orig_moves._action_cancel() order.write({"state": "canceled"}) order.move_ids._action_cancel() return True def _get_price_unit(self): """The price unit corresponds to the cost of that product""" self.ensure_one() if self.reference_move_id: price_unit = self.reference_move_id.price_unit else: price_unit = self.product_id.with_company(self.company_id).standard_price return price_unit @api.onchange("product_id") def _onchange_product_id(self): result = {} if not self.product_id: return result self.uom_id = self.product_id.uom_id.id if not self.type: self.type = self._get_default_type() self.price_unit = self._get_price_unit() if self.type == "customer": self.operation_id = ( self.product_id.rma_customer_operation_id or self.product_id.categ_id.rma_customer_operation_id ) else: self.operation_id = ( self.product_id.rma_supplier_operation_id or self.product_id.categ_id.rma_supplier_operation_id ) if self.lot_id.product_id != self.product_id: self.lot_id = False return result @api.onchange("operation_id") def _onchange_operation_id(self): result = {} if not self.operation_id: return result self.receipt_policy = self.operation_id.receipt_policy self.delivery_policy = self.operation_id.delivery_policy self.customer_to_supplier = ( self.rma_id.customer_to_supplier or self.operation_id.customer_to_supplier ) self.supplier_to_customer = ( self.rma_id.supplier_to_customer or self.operation_id.supplier_to_customer ) self.in_warehouse_id = ( self.rma_id.in_warehouse_id or self.operation_id.in_warehouse_id ) self.out_warehouse_id = ( self.rma_id.out_warehouse_id or self.operation_id.out_warehouse_id ) self.location_id = ( self.rma_id.location_id or self.operation_id.location_id or self.in_warehouse_id.lot_rma_id ) self.in_route_id = self.rma_id.in_route_id or self.operation_id.in_route_id self.out_route_id = self.rma_id.out_route_id or self.operation_id.out_route_id return result @api.depends("operation_id") def _compute_in_route_id(self): for rec in self: rec.in_route_id = rec.operation_id.in_route_id @api.depends("operation_id") def _compute_out_route_id(self): for rec in self: rec.out_route_id = rec.operation_id.out_route_id @api.onchange("customer_to_supplier", "type") def _onchange_receipt_policy(self): if self.type == "supplier" and self.customer_to_supplier: self.receipt_policy = "no" elif self.type == "customer" and self.supplier_to_customer: self.delivery_policy = "no" @api.onchange("lot_id") def _onchange_lot_id(self): product = self.lot_id.product_id if product: self.product_id = product @api.depends("product_id") def _compute_uom_id(self): for rec in self: if rec.product_id: rec.uom_id = rec.product_id.uom_id def action_view_in_shipments(self): action = self.env.ref("stock.action_picking_tree_all") result = action.sudo().read()[0] shipments = self.env["stock.picking"] for line in self: shipments |= line._get_in_pickings() # choose the view_mode accordingly if len(shipments) != 1: result["domain"] = "[('id', 'in', " + str(shipments.ids) + ")]" elif len(shipments) == 1: res = self.env.ref("stock.view_picking_form", False) result["views"] = [(res and res.id or False, "form")] result["res_id"] = shipments.ids[0] return result def action_view_out_shipments(self): action = self.env.ref("stock.action_picking_tree_all") result = action.sudo().read()[0] shipments = self.env["stock.picking"] for line in self: shipments |= line._get_out_pickings() # choose the view_mode accordingly if len(shipments) != 1: result["domain"] = "[('id', 'in', " + str(shipments.ids) + ")]" elif len(shipments) == 1: res = self.env.ref("stock.view_picking_form", False) result["views"] = [(res and res.id or False, "form")] result["res_id"] = shipments.ids[0] return result def action_view_rma_lines(self): if self.type == "customer": # from customer we link to supplier rma action = self.env.ref("rma.action_rma_supplier_lines") rma_lines = self.supplier_rma_line_ids res = self.env.ref("rma.view_rma_line_supplier_form", False) else: # from supplier we link to customer rma action = self.env.ref("rma.action_rma_customer_lines") rma_lines = self.customer_rma_id res = self.env.ref("rma.view_rma_line_form", False) result = action.sudo().read()[0] # choose the view_mode accordingly if rma_lines and len(rma_lines) != 1: result["domain"] = [("id", "in", rma_lines.ids)] elif len(rma_lines) == 1: result["views"] = [(res and res.id or False, "form")] result["res_id"] = rma_lines.id return result @api.constrains("partner_id", "rma_id") def _check_partner_id(self): if self.rma_id and self.partner_id != self.rma_id.partner_id: raise ValidationError( _("Group partner and RMA's partner must be the same.") )