diff --git a/rma/README.rst b/rma/README.rst index 107d9bfd5..67ebcdab6 100644 --- a/rma/README.rst +++ b/rma/README.rst @@ -7,7 +7,7 @@ Return Merchandise Authorization Management !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ff393a4ae1a4e373490a3d9129969c7a98f022e1e3b823a1d8653ed23c97ce55 + !! source digest: sha256:4c6046c0f48cbcedbf0938b09da85d78cb0435ccf2327a138e572cc8d432ac2e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -165,6 +165,7 @@ Contributors * Chafique Delli * Giovanni Serra - Ooops +* Michael Tietz (MT Software) Maintainers ~~~~~~~~~~~ diff --git a/rma/hooks.py b/rma/hooks.py index 83b95586d..bde5954f9 100644 --- a/rma/hooks.py +++ b/rma/hooks.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import SUPERUSER_ID, api @@ -24,7 +25,9 @@ def _get_next_picking_type_color(): def create_rma_locations(warehouse): stock_location = env["stock.location"] if not warehouse.rma_loc_id: - rma_location_vals = warehouse._get_rma_location_values() + rma_location_vals = warehouse._get_rma_location_values( + {"company_id": warehouse.company_id.id}, warehouse.code + ) warehouse.rma_loc_id = ( stock_location.with_context(active_test=False) .create(rma_location_vals) @@ -61,11 +64,19 @@ def create_rma_picking_types(whs): whs.rma_in_type_id.return_picking_type_id = whs.rma_out_type_id.id whs.rma_out_type_id.return_picking_type_id = whs.rma_in_type_id.id + def create_rma_routes(warehouses): + """Create initially rma in/out stock.location.routes and stock.rules""" + warehouses = warehouses.with_context(rma_post_init_hook=True) + for wh in warehouses: + route_vals = wh._create_or_update_route() + wh.write(route_vals) + # Create rma locations and picking types warehouses = env["stock.warehouse"].search([]) for warehouse in warehouses: create_rma_locations(warehouse) create_rma_picking_types(warehouse) + create_rma_routes(warehouses) # Create rma sequence per company for company in env["res.company"].search([]): company.create_rma_index() diff --git a/rma/models/rma.py b/rma/models/rma.py index 811d2546d..c9c2ccc91 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -1,7 +1,9 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging -from collections import Counter +from collections import defaultdict +from itertools import groupby from odoo import _, api, fields, models from odoo.exceptions import AccessError, ValidationError @@ -304,18 +306,8 @@ def _domain_location_id(self): ) def _compute_delivery_picking_count(self): - # It is enough to count the moves to know how many pickings - # there are because there will be a unique move linked to the - # same picking and the same rma. - rma_data = self.env["stock.move"].read_group( - [("rma_id", "in", self.ids)], - ["rma_id", "picking_id"], - ["rma_id", "picking_id"], - lazy=False, - ) - mapped_data = Counter(map(lambda r: r["rma_id"][0], rma_data)) - for record in self: - record.delivery_picking_count = mapped_data.get(record.id, 0) + for rma in self: + rma.delivery_picking_count = len(rma.delivery_move_ids.picking_id) @api.depends( "delivery_move_ids", @@ -650,21 +642,101 @@ def action_rma_send(self): } def _add_message_subscribe_partner(self): + self.ensure_one() if self.partner_id and self.partner_id not in self.message_partner_ids: self.message_subscribe([self.partner_id.id]) + def _product_is_storable(self, product=None): + product = product or self.product_id + return product.type in ["product", "consu"] + + def _prepare_procurement_group_values(self): + return { + "move_type": "direct", + "partner_id": self and self.partner_shipping_id.id or False, + "name": self and ", ".join(self.mapped("name")) or False, + } + + def _prepare_procurement_values( + self, warehouse=None, scheduled_date=None, group=None + ): + self.ensure_one() + group = group or self.procurement_group_id + if not group: + group = self.env["procurement.group"].create( + self._prepare_procurement_group_values() + ) + return { + "company_id": self.company_id, + "group_id": group, + "date_planned": scheduled_date or fields.Datetime.now(), + "warehouse_id": warehouse or self.warehouse_id, + "partner_id": group.partner_id.id, + "priority": self.priority, + } + + def _prepare_reception_procurement_values(self, group=None): + values = self._prepare_procurement_values(group=group) + values.update( + { + "rma_receiver_ids": [(6, 0, self.ids)], + } + ) + if self.move_id: + values.update( + { + "origin_returned_move_id": self.move_id.id, + } + ) + return values + + def _create_reception_procurement_group(self): + return self.env["procurement.group"].create( + self._prepare_procurement_group_values() + ) + + def _prepare_reception_procurement(self): + self.ensure_one() + group = self.procurement_group_id + if not group: + group = self._create_reception_procurement_group() + product = self.product_id + return self.env["procurement.group"].Procurement( + product, + self.product_uom_qty, + self.product_uom, + self.location_id, + product.display_name, + group.name, + self.company_id, + self._prepare_reception_procurement_values(group), + ) + + def _prepare_reception_procurements(self): + procurements = [] + for rma in self: + if not rma._product_is_storable(): + continue + procurements.append(rma._prepare_reception_procurement()) + return procurements + + def _create_receptions(self): + procurements = self._prepare_reception_procurements() + if procurements: + self.env["procurement.group"].run(procurements) + def action_confirm(self): """Invoked when 'Confirm' button in rma form view is clicked.""" - self.ensure_one() self._ensure_required_fields() - if self.state == "draft": - if self.picking_id: - reception_move = self._create_receptions_from_picking() - else: - reception_move = self._create_receptions_from_product() - self.write({"reception_move_id": reception_move.id, "state": "confirmed"}) - self._add_message_subscribe_partner() - self._send_confirmation_email() + self = self.filtered(lambda rma: rma.state == "draft") + if not self: + return + self._create_receptions() + self.reception_move_id.picking_id.action_assign() + self.write({"state": "confirmed"}) + for rma in self: + rma._add_message_subscribe_partner() + self._send_confirmation_email() def action_refund(self): """Invoked when 'Refund' button in rma form view is clicked @@ -719,11 +791,8 @@ def action_replace(self): self._ensure_can_be_replaced() # Force active_id to avoid issues when coming from smart buttons # in other models - action = ( - self.env.ref("rma.rma_delivery_wizard_action") - .sudo() - .with_context(active_id=self.id) - .read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id( + "rma.rma_delivery_wizard_action" ) action["name"] = "Replace product(s)" action["context"] = dict(self.env.context) @@ -742,11 +811,8 @@ def action_return(self): self._ensure_can_be_returned() # Force active_id to avoid issues when coming from smart buttons # in other models - action = ( - self.env.ref("rma.rma_delivery_wizard_action") - .sudo() - .with_context(active_id=self.id) - .read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id( + "rma.rma_delivery_wizard_action" ) action["context"] = dict(self.env.context) action["context"].update( @@ -762,11 +828,8 @@ def action_split(self): self._ensure_can_be_split() # Force active_id to avoid issues when coming from smart buttons # in other models - action = ( - self.env.ref("rma.rma_split_wizard_action") - .sudo() - .with_context(active_id=self.id) - .read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id( + "rma.rma_split_wizard_action" ) action["context"] = dict(self.env.context) action["context"].update(active_id=self.id, active_ids=self.ids) @@ -778,11 +841,8 @@ def action_finish(self): self._ensure_can_be_returned() # Force active_id to avoid issues when coming from smart buttons # in other models - action = ( - self.env.ref("rma.rma_finalization_wizard_action") - .sudo() - .with_context(active_id=self.id) - .read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id( + "rma.rma_finalization_wizard_action" ) action["context"] = dict(self.env.context) action["context"].update(active_id=self.id, active_ids=self.ids) @@ -790,7 +850,7 @@ def action_finish(self): def action_cancel(self): """Invoked when 'Cancel' button in rma form view is clicked.""" - self.mapped("reception_move_id")._action_cancel() + self.reception_move_id._action_cancel() self.write({"state": "cancelled"}) def action_draft(self): @@ -815,25 +875,28 @@ def action_preview(self): "url": self.get_portal_url(), } - def action_view_receipt(self): - """Invoked when 'Receipt' smart button in rma form view is clicked.""" + def _action_view_pickings(self, pickings): self.ensure_one() # Force active_id to avoid issues when coming from smart buttons # in other models - action = ( - self.env.ref("stock.action_picking_tree_all") - .sudo() - .with_context(active_id=self.id) - .read()[0] - ) - action.update( - res_id=self.reception_move_id.picking_id.id, - view_mode="form", - view_id=False, - views=False, + action = self.env["ir.actions.act_window"]._for_xml_id( + "stock.action_picking_tree_all" ) + if len(pickings) > 1: + action["domain"] = [("id", "in", pickings.ids)] + elif pickings: + action.update( + res_id=pickings.id, + view_mode="form", + view_id=False, + views=False, + ) return action + def action_view_receipt(self): + """Invoked when 'Receipt' smart button in rma form view is clicked.""" + return self._action_view_pickings(self.reception_move_id.picking_id) + def action_view_refund(self): """Invoked when 'Refund' smart button in rma form view is clicked.""" self.ensure_one() @@ -849,23 +912,7 @@ def action_view_refund(self): def action_view_delivery(self): """Invoked when 'Delivery' smart button in rma form view is clicked.""" - action = ( - self.env.ref("stock.action_picking_tree_all") - .sudo() - .with_context(active_id=self.id) - .read()[0] - ) - picking = self.delivery_move_ids.mapped("picking_id") - if len(picking) > 1: - action["domain"] = [("id", "in", picking.ids)] - elif picking: - action.update( - res_id=picking.id, - view_mode="form", - view_id=False, - views=False, - ) - return action + return self._action_view_pickings(self.delivery_move_ids.picking_id) # Validation business methods def _ensure_required_fields(self): @@ -972,72 +1019,6 @@ def _ensure_qty_to_extract(self, qty, uom): ) ) - # Reception business methods - def _create_receptions_from_picking(self): - self.ensure_one() - stock_return_picking_form = Form( - self.env["stock.return.picking"].with_context( - active_ids=self.picking_id.ids, - active_id=self.picking_id.id, - active_model="stock.picking", - ) - ) - if self.location_id: - stock_return_picking_form.location_id = self.location_id - return_wizard = stock_return_picking_form.save() - return_wizard.product_return_moves.filtered( - lambda r: r.move_id != self.move_id - ).unlink() - return_line = return_wizard.product_return_moves - return_line.update( - { - "quantity": self.product_uom_qty, - # The to_refund field is now True by default, which isn't right in the RMA - # creation context. - "to_refund": False, - } - ) - # set_rma_picking_type is to override the copy() method of stock - # picking and change the default picking type to rma picking type. - picking_action = return_wizard.with_context( - set_rma_picking_type=True - ).create_returns() - picking_id = picking_action["res_id"] - picking = self.env["stock.picking"].browse(picking_id) - picking.origin = "{} ({})".format(self.name, picking.origin) - move = picking.move_lines - move.priority = self.priority - return move - - def _create_receptions_from_product(self): - self.ensure_one() - picking_form = Form( - recordp=self.env["stock.picking"].with_context( - default_picking_type_id=self.warehouse_id.rma_in_type_id.id - ), - view="stock.view_picking_form", - ) - self._prepare_picking(picking_form) - picking = picking_form.save() - picking.action_confirm() - picking.action_assign() - picking.message_post_with_view( - "mail.message_origin_link", - values={"self": picking, "origin": self}, - subtype_id=self.env.ref("mail.mt_note").id, - ) - return picking.move_lines - - def _prepare_picking(self, picking_form): - picking_form.origin = self.name - picking_form.partner_id = self.partner_shipping_id - picking_form.location_id = self.partner_shipping_id.property_stock_customer - picking_form.location_dest_id = self.location_id - with picking_form.move_ids_without_package.new() as move_form: - move_form.product_id = self.product_id - move_form.product_uom_qty = self.product_uom_qty - move_form.product_uom = self.product_uom - # Extract business methods def extract_quantity(self, qty, uom): self.ensure_one() @@ -1124,63 +1105,112 @@ def _get_extra_refund_line_vals(self): """Override to write aditional stuff into the refund line""" return {} - # Returning business methods - def create_return(self, scheduled_date, qty=None, uom=None): - """Intended to be invoked by the delivery wizard""" + def _delivery_should_be_grouped(self): + """Checks if the rmas should be grouped for the delivery process""" group_returns = self.env.company.rma_return_grouping if "rma_return_grouping" in self.env.context: group_returns = self.env.context.get("rma_return_grouping") - self._ensure_can_be_returned() - self._ensure_qty_to_return(qty, uom) - group_dict = {} - rmas_to_return = self.filtered("can_be_returned") - for record in rmas_to_return: - key = ( - record.partner_shipping_id.id, - record.company_id.id, - record.warehouse_id, + return group_returns + + def _delivery_group_key(self): + """Returns a key by which the rmas should be grouped for the delivery process""" + self.ensure_one() + return (self.partner_shipping_id.id, self.company_id.id, self.warehouse_id.id) + + def _group_delivery_if_needed(self): + """Groups the given rmas by the returned key from _delivery_group_key + by setting the procurement_group_id on the each rma if there is not yet on set""" + if not self._delivery_should_be_grouped(): + return + grouped_rmas = groupby( + sorted(self, key=lambda rma: rma._delivery_group_key()), + key=lambda rma: [rma._delivery_group_key()], + ) + for _group, rmas in grouped_rmas: + rmas = ( + self.browse() + .concat(*list(rmas)) + .filtered(lambda rma: not rma.procurement_group_id) ) - group_dict.setdefault(key, self.env["rma"]) - group_dict[key] |= record - if group_returns: - grouped_rmas = group_dict.values() - else: - grouped_rmas = rmas_to_return - for rmas in grouped_rmas: - origin = ", ".join(rmas.mapped("name")) - rma_out_type = rmas[0].warehouse_id.rma_out_type_id - picking_form = Form( - recordp=self.env["stock.picking"].with_context( - default_picking_type_id=rma_out_type.id - ), - view="stock.view_picking_form", + if not rmas: + continue + proc_group = rmas._create_delivery_procurement_group() + rmas.write({"procurement_group_id": proc_group.id}) + + def _prepare_outgoing_procurement_values(self, warehouse=None, scheduled_date=None): + values = self._prepare_procurement_values(warehouse, scheduled_date) + values.update({"rma_id": self.id}) + return values + + def _prepare_delivery_procurement_values(self, scheduled_date=None): + values = self._prepare_outgoing_procurement_values( + scheduled_date=scheduled_date + ) + values.update( + { + "move_orig_ids": [(6, 0, self.reception_move_id.ids)], + } + ) + return values + + def _prepare_delivery_procurement(self, scheduled_date=None, qty=None, uom=None): + self.ensure_one() + values = self._prepare_delivery_procurement_values(scheduled_date) + group = values.get("group_id") + product = self.product_id + return self.env["procurement.group"].Procurement( + product, + qty or self.product_uom_qty, + uom or self.product_uom, + self.partner_shipping_id.property_stock_customer, + product.display_name, + group.name, + self.company_id, + values, + ) + + def _create_delivery_procurement_group(self): + return self.env["procurement.group"].create( + self._prepare_procurement_group_values() + ) + + def _prepare_delivery_procurements(self, scheduled_date=None, qty=None, uom=None): + self._group_delivery_if_needed() + procurements = [] + for rma in self: + if not rma.procurement_group_id: + rma.procurement_group_id = rma._create_delivery_procurement_group() + procurements.append( + rma._prepare_delivery_procurement(scheduled_date, qty, uom) ) - rmas[0]._prepare_returning_picking(picking_form, origin) - picking = picking_form.save() - for rma in rmas: - with picking_form.move_ids_without_package.new() as move_form: - rma._prepare_returning_move(move_form, scheduled_date, qty, uom) - # rma_id is not present in the form view, so we need to get - # the 'values to save' to add the rma id and use the - # create method intead of save the form. - picking_vals = picking_form._values_to_save(all_fields=True) - move_vals = picking_vals["move_ids_without_package"][-1][2] - move_vals.update( - picking_id=picking.id, - rma_id=rma.id, - move_orig_ids=[(4, rma.reception_move_id.id)], - company_id=picking.company_id.id, - ) - if "product_qty" in move_vals: - move_vals.pop("product_qty") - self.env["stock.move"].sudo().create(move_vals) - rma.message_post( - body=_( - 'Return: %(name)s has been created.' - ) - % ({"id": picking.id, "name": picking.name}) + return procurements + + def _create_delivery(self, scheduled_date=None, qty=None, uom=None): + procurements = self._prepare_delivery_procurements(scheduled_date, qty, uom) + if procurements: + self.env["procurement.group"].run(procurements) + + # Returning business methods + def create_return(self, scheduled_date, qty=None, uom=None): + """Intended to be invoked by the delivery wizard""" + self._ensure_can_be_returned() + self._ensure_qty_to_return(qty, uom) + rmas_to_return = self.filtered( + lambda rma: rma.can_be_returned and rma._product_is_storable() + ) + rmas_to_return._create_delivery(scheduled_date, qty, uom) + pickings = defaultdict(lambda: self.browse()) + for rma in rmas_to_return: + picking = rma.delivery_move_ids.picking_id.sorted("id", reverse=True)[0] + pickings[picking] |= rma + rma.message_post( + body=_( + 'Return: %(name)s has been created.' ) + % ({"id": picking.id, "name": picking.name}) + ) + for picking, rmas in pickings.items(): picking.action_confirm() picking.action_assign() picking.message_post_with_view( @@ -1190,26 +1220,60 @@ def create_return(self, scheduled_date, qty=None, uom=None): ) rmas_to_return.write({"state": "waiting_return"}) - def _prepare_returning_picking(self, picking_form, origin=None): - picking_form.picking_type_id = self.warehouse_id.rma_out_type_id - picking_form.origin = origin or self.name - picking_form.partner_id = self.partner_shipping_id + def _prepare_replace_procurement_values(self, warehouse=None, scheduled_date=None): + return self._prepare_outgoing_procurement_values(warehouse, scheduled_date) + + def _create_replace_procurement_group(self): + return self.env["procurement.group"].create( + self._prepare_procurement_group_values() + ) + + def _prepare_replace_procurement( + self, warehouse, scheduled_date, product, qty, uom + ): + if not self.procurement_group_id: + self.procurement_group_id = self._create_replace_procurement_group() + + values = self._prepare_replace_procurement_values(warehouse, scheduled_date) + group = values.get("group_id") + return self.env["procurement.group"].Procurement( + product, + qty, + uom, + self.partner_shipping_id.property_stock_customer, + product.display_name, + group.name, + self.company_id, + values, + ) - def _prepare_returning_move( - self, move_form, scheduled_date, quantity=None, uom=None + def _prepare_replace_procurements( + self, warehouse, scheduled_date, product, qty, uom ): - move_form.product_id = self.product_id - move_form.product_uom_qty = quantity or self.product_uom_qty - move_form.product_uom = uom or self.product_uom - move_form.date = scheduled_date + procurements = [] + for rma in self: + if not rma._product_is_storable(product): + continue + + procurements.append( + rma._prepare_replace_procurement( + warehouse, scheduled_date, product, qty, uom + ) + ) + return procurements + + def _create_replace(self, warehouse, scheduled_date, product, qty, uom): + procurements = self._prepare_replace_procurements( + warehouse, scheduled_date, product, qty, uom + ) + self.env["procurement.group"].run(procurements) # Replacing business methods def create_replace(self, scheduled_date, warehouse, product, qty, uom): """Intended to be invoked by the delivery wizard""" - self.ensure_one() self._ensure_can_be_replaced() moves_before = self.delivery_move_ids - self._action_launch_stock_rule(scheduled_date, warehouse, product, qty, uom) + self._create_replace(warehouse, scheduled_date, product, qty, uom) new_moves = self.delivery_move_ids - moves_before body = "" # The product replacement could explode into several moves like in the case of @@ -1233,6 +1297,12 @@ def create_replace(self, scheduled_date, warehouse, product, qty, uom): ) + "\n" ) + for rma in self: + rma._add_replace_message(body, qty, uom) + self.write({"state": "waiting_replacement"}) + + def _add_replace_message(self, body, qty, uom): + self.ensure_one() self.message_post( body=body or _( @@ -1245,74 +1315,13 @@ def create_replace(self, scheduled_date, warehouse, product, qty, uom): ) % ( { - "id": product.id, - "name": product.display_name, + "id": self.id, + "name": self.display_name, "qty": qty, "uom": uom.name, } ) ) - if self.state != "waiting_replacement": - self.state = "waiting_replacement" - - def _action_launch_stock_rule( - self, - scheduled_date, - warehouse, - product, - qty, - uom, - ): - """Creates a delivery picking and launch stock rule. It is invoked by: - rma.create_replace - """ - self.ensure_one() - if self.product_id.type not in ("consu", "product"): - return - if not self.procurement_group_id: - self.procurement_group_id = ( - self.env["procurement.group"] - .create( - { - "name": self.name, - "move_type": "direct", - "partner_id": self.partner_shipping_id.id, - } - ) - .id - ) - values = self._prepare_procurement_values( - self.procurement_group_id, scheduled_date, warehouse - ) - procurement = self.env["procurement.group"].Procurement( - product, - qty, - uom, - self.partner_shipping_id.property_stock_customer, - self.product_id.display_name, - self.procurement_group_id.name, - self.company_id, - values, - ) - self.env["procurement.group"].run([procurement]) - return True - - def _prepare_procurement_values( - self, - group_id, - scheduled_date, - warehouse, - ): - self.ensure_one() - return { - "company_id": self.company_id, - "group_id": group_id, - "date_planned": scheduled_date, - "warehouse_id": warehouse, - "partner_id": self.partner_shipping_id.id, - "rma_id": self.id, - "priority": self.priority, - } # Mail business methods def _creation_subtype(self): diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py index 8d9925615..2f17eb5c8 100644 --- a/rma/models/stock_move.py +++ b/rma/models/stock_move.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models @@ -22,7 +23,7 @@ class StockMove(models.Model): string="RMA receivers", copy=False, ) - # RMA that create the delivery movement to the customer + # RMA that create the out move rma_id = fields.Many2one( comodel_name="rma", string="RMA return", @@ -32,8 +33,8 @@ class StockMove(models.Model): def unlink(self): # A stock user could have no RMA permissions, so the ids wouldn't # be accessible due to record rules. - rma_receiver = self.sudo().mapped("rma_receiver_ids") - rma = self.sudo().mapped("rma_id") + rma_receiver = self.sudo().rma_receiver_ids + rma = self.sudo().rma_id res = super().unlink() rma_receiver.filtered(lambda x: x.state != "cancelled").write( {"state": "draft"} @@ -103,38 +104,22 @@ def _prepare_move_split_vals(self, qty): res["rma_id"] = self.sudo().rma_id.id return res - def _prepare_return_rma_vals(self, original_picking): - """hook method for preparing an RMA from the 'return picking wizard'.""" - self.ensure_one() - partner = original_picking.partner_id - if hasattr(original_picking, "sale_id") and original_picking.sale_id: - partner_invoice_id = original_picking.sale_id.partner_invoice_id.id - partner_shipping_id = original_picking.sale_id.partner_shipping_id.id - else: - partner_invoice_id = partner.address_get(["invoice"]).get("invoice", False) - partner_shipping_id = partner.address_get(["delivery"]).get( - "delivery", False - ) - return { - "user_id": self.env.user.id, - "partner_id": partner.id, - "partner_shipping_id": partner_shipping_id, - "partner_invoice_id": partner_invoice_id, - "origin": original_picking.name, - "picking_id": original_picking.id, - "move_id": self.origin_returned_move_id.id, - "product_id": self.origin_returned_move_id.product_id.id, - "product_uom_qty": self.product_uom_qty, - "product_uom": self.product_uom.id, - "reception_move_id": self.id, - "company_id": self.company_id.id, - "location_id": self.location_dest_id.id, - "state": "confirmed", - } + def _prepare_procurement_values(self): + res = super()._prepare_procurement_values() + if self.rma_id: + res["rma_id"] = self.rma_id.id + return res class StockRule(models.Model): _inherit = "stock.rule" def _get_custom_move_fields(self): - return super()._get_custom_move_fields() + ["rma_id"] + move_fields = super()._get_custom_move_fields() + move_fields += [ + "rma_id", + "origin_returned_move_id", + "move_orig_ids", + "rma_receiver_ids", + ] + return move_fields diff --git a/rma/models/stock_picking.py b/rma/models/stock_picking.py index 242871d5b..8617d2872 100644 --- a/rma/models/stock_picking.py +++ b/rma/models/stock_picking.py @@ -29,7 +29,7 @@ def copy(self, default=None): def action_view_rma(self): self.ensure_one() - action = self.sudo().env.ref("rma.rma_action").read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id("rma.rma_action") rma = self.move_lines.mapped("rma_ids") if len(rma) == 1: action.update( diff --git a/rma/models/stock_warehouse.py b/rma/models/stock_warehouse.py index 1398faecb..f0464124b 100644 --- a/rma/models/stock_warehouse.py +++ b/rma/models/stock_warehouse.py @@ -1,7 +1,8 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models +from odoo import _, fields, models class StockWarehouse(models.Model): @@ -27,35 +28,39 @@ class StockWarehouse(models.Model): comodel_name="stock.location", string="RMA Location", ) + rma_in_route_id = fields.Many2one("stock.location.route", "RMA in Route") + rma_out_route_id = fields.Many2one("stock.location.route", "RMA out Route") - @api.model_create_multi - def create(self, vals_list): - """To create an RMA location and link it with a new warehouse, - this method is overridden instead of '_get_locations_values' - method because the locations that are created with the - values ​​returned by that method are forced to be children - of view_location_id, and we don't want that. - """ - res = super().create(vals_list) - stock_location = self.env["stock.location"] - for record in res: - rma_location_vals = record._get_rma_location_values() - record.rma_loc_id = stock_location.create(rma_location_vals).id - return res - - def _get_rma_location_values(self): + def _get_rma_location_values(self, vals, code=False): """this method is intended to be used by 'create' method to create a new RMA location to be linked to a new warehouse. """ + company_id = vals.get( + "company_id", self.default_get(["company_id"])["company_id"] + ) + code = vals.get("code") or code or "" + code = code.replace(" ", "").upper() + view_location_id = vals.get("view_location_id") + view_location = ( + view_location_id + and self.view_location_id.browse(view_location_id) + or self.view_location_id + ) return { - "name": self.view_location_id.name, + "name": view_location.name, "active": True, "return_location": True, "usage": "internal", - "company_id": self.company_id.id, + "company_id": company_id, "location_id": self.env.ref("rma.stock_location_rma").id, + "barcode": self._valid_barcode(code + "-RMA", company_id), } + def _get_locations_values(self, vals, code=False): + res = super()._get_locations_values(vals, code) + res["rma_loc_id"] = self._get_rma_location_values(vals, code) + return res + def _get_sequence_values(self): values = super()._get_sequence_values() values.update( @@ -138,3 +143,70 @@ def _create_or_update_sequences_and_picking_types(self): {"return_picking_type_id": data.get("rma_out_type_id", False)} ) return data + + def _get_routes_values(self): + res = super()._get_routes_values() + rma_routes = { + "rma_in_route_id": { + "routing_key": "rma_in", + "depends": ["active"], + "route_update_values": { + "name": self._format_routename("RMA In"), + "active": self.active, + }, + "route_create_values": { + "warehouse_selectable": True, + "company_id": self.company_id.id, + "sequence": 100, + }, + "rules_values": { + "active": True, + }, + }, + "rma_out_route_id": { + "routing_key": "rma_out", + "depends": ["active"], + "route_update_values": { + "name": self._format_routename("RMA Out"), + "active": self.active, + }, + "route_create_values": { + "warehouse_selectable": True, + "company_id": self.company_id.id, + "sequence": 110, + }, + "rules_values": { + "active": True, + }, + }, + } + if self.env.context.get("rma_post_init_hook"): + return rma_routes + res.update(rma_routes) + return res + + def get_rules_dict(self): + res = super().get_rules_dict() + customer_loc, supplier_loc = self._get_partner_locations() + for warehouse in self: + res[warehouse.id].update( + { + "rma_in": [ + self.Routing( + customer_loc, + warehouse.rma_loc_id, + warehouse.rma_in_type_id, + "pull", + ) + ], + "rma_out": [ + self.Routing( + warehouse.rma_loc_id, + customer_loc, + warehouse.rma_out_type_id, + "pull", + ) + ], + } + ) + return res diff --git a/rma/readme/CONTRIBUTORS.rst b/rma/readme/CONTRIBUTORS.rst index 75646b6d3..1ebe5ca2f 100644 --- a/rma/readme/CONTRIBUTORS.rst +++ b/rma/readme/CONTRIBUTORS.rst @@ -7,3 +7,4 @@ * Chafique Delli * Giovanni Serra - Ooops +* Michael Tietz (MT Software) diff --git a/rma/static/description/index.html b/rma/static/description/index.html index 48bf9f1e2..f308bfd83 100644 --- a/rma/static/description/index.html +++ b/rma/static/description/index.html @@ -366,7 +366,7 @@

Return Merchandise Authorization Management

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:ff393a4ae1a4e373490a3d9129969c7a98f022e1e3b823a1d8653ed23c97ce55 +!! source digest: sha256:4c6046c0f48cbcedbf0938b09da85d78cb0435ccf2327a138e572cc8d432ac2e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/rma Translate me on Weblate Try me on Runboat

This module allows you to manage Return Merchandise Authorization (RMA). @@ -518,6 +518,7 @@

Contributors

  • Chafique Delli <chafique.delli@akretion.com>
  • Giovanni Serra - Ooops <giovanni@ooops404.com>
  • +
  • Michael Tietz (MT Software) <mtietz@mt-software.de>
  • diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index 63363b290..cd027ad9c 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.exceptions import UserError, ValidationError @@ -146,6 +147,23 @@ def _create_delivery(self): class TestRmaCase(TestRma): + def test_rma_replace_pick_ship(self): + warehouse = self.env.ref("stock.warehouse0") + warehouse.write({"delivery_steps": "pick_ship"}) + rma = self._create_rma(self.partner, self.product, 1, self.rma_loc) + rma.action_confirm() + rma.reception_move_id.quantity_done = 1 + rma.reception_move_id.picking_id._action_done() + self.assertEqual(rma.reception_move_id.picking_id.state, "done") + self.assertEqual(rma.state, "received") + res = rma.action_replace() + wizard_form = Form(self.env[res["res_model"]].with_context(**res["context"])) + wizard_form.product_id = self.product + wizard_form.product_uom_qty = rma.product_uom_qty + wizard = wizard_form.save() + wizard.action_deliver() + self.assertEqual(rma.delivery_picking_count, 2) + def test_onchange(self): rma_form = Form(self.env["rma"]) # If partner changes, the invoice address is set @@ -358,7 +376,7 @@ def test_mass_refund(self): # line of refund_1 self.assertEqual(len(refund_1.invoice_line_ids), 3) self.assertEqual( - refund_1.invoice_line_ids.mapped("rma_id"), + refund_1.invoice_line_ids.rma_id, (rma_1 | rma_2 | rma_3), ) self.assertEqual( @@ -592,7 +610,7 @@ def test_mass_return_to_customer(self): # line of picking_1 self.assertEqual(len(pick_1.move_lines), 3) self.assertEqual( - pick_1.move_lines.mapped("rma_id"), + pick_1.move_lines.rma_id, (rma_1 | rma_2 | rma_3), ) self.assertEqual( @@ -663,14 +681,14 @@ def test_rma_from_picking_return(self): origin_moves = origin_delivery.move_lines self.assertTrue(origin_moves[0].rma_ids) self.assertTrue(origin_moves[1].rma_ids) - rmas = origin_moves.mapped("rma_ids") + rmas = origin_moves.rma_ids self.assertEqual(rmas.mapped("state"), ["confirmed"] * 2) # Each reception move is linked one of the generated RMAs reception = self.env["stock.picking"].browse(picking_action["res_id"]) reception_moves = reception.move_lines self.assertTrue(reception_moves[0].rma_receiver_ids) self.assertTrue(reception_moves[1].rma_receiver_ids) - self.assertEqual(reception_moves.mapped("rma_receiver_ids"), rmas) + self.assertEqual(reception_moves.rma_receiver_ids, rmas) # Validate the reception picking to set rmas to 'received' state reception_moves[0].quantity_done = reception_moves[0].product_uom_qty reception_moves[1].quantity_done = reception_moves[1].product_uom_qty diff --git a/rma/wizard/stock_picking_return.py b/rma/wizard/stock_picking_return.py index 7e16ea6dd..3f654cd92 100644 --- a/rma/wizard/stock_picking_return.py +++ b/rma/wizard/stock_picking_return.py @@ -1,8 +1,25 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from copy import deepcopy from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools import float_compare + + +class ReturnPickingLine(models.TransientModel): + _inherit = "stock.return.picking.line" + + def _prepare_rma_values(self): + self.ensure_one() + return { + "move_id": self.move_id.id, + "product_id": self.move_id.product_id.id, + "product_uom_qty": self.quantity, + "product_uom": self.product_id.uom_id.id, + "location_id": self.wizard_id.location_id.id or self.move_id.location_id.id, + } class ReturnPicking(models.TransientModel): @@ -44,6 +61,56 @@ def _onchange_create_rma(self): ] return {"domain": {"location_id": rma_loc_domain}} + def _prepare_rma_partner_values(self): + self.ensure_one() + partner = self.picking_id.partner_id + partner_address = partner.address_get(["invoice", "delivery"]) + partner_invoice_id = partner_address.get("invoice", False) + partner_shipping_id = partner_address.get("delivery", False) + return ( + partner, + partner_invoice_id and partner.browse(partner_invoice_id) or partner, + partner_shipping_id and partner.browse(partner_shipping_id) or partner, + ) + + def _prepare_rma_values(self): + partner, partner_invoice, partner_shipping = self._prepare_rma_partner_values() + origin = self.picking_id.name + group_vals = self.env["rma"]._prepare_procurement_group_values() + group_vals.update( + { + "partner_id": partner_shipping.id, + "name": origin, + } + ) + group = self.env["procurement.group"].create(group_vals) + return { + "user_id": self.env.user.id, + "partner_id": partner.id, + "partner_shipping_id": partner_shipping.id, + "partner_invoice_id": partner_invoice.id, + "origin": origin, + "picking_id": self.picking_id.id, + "company_id": self.company_id.id, + "procurement_group_id": group.id, + } + + def _prepare_rma_vals_list(self): + vals_list = [] + for return_picking in self: + global_vals = return_picking._prepare_rma_values() + for line in return_picking.product_return_moves: + if ( + not line.move_id + or float_compare(line.quantity, 0, line.product_id.uom_id.rounding) + <= 0 + ): + continue + vals = deepcopy(global_vals) + vals.update(line._prepare_rma_values()) + vals_list.append(vals) + return vals_list + def create_returns(self): """Override create_returns method for creating one or more 'confirmed' RMAs after return a delivery picking in case @@ -53,10 +120,6 @@ def create_returns(self): as the 'Receipt'. """ if self.create_rma: - # set_rma_picking_type is to override the copy() method of stock - # picking and change the default picking type to rma picking type - self_with_context = self.with_context(set_rma_picking_type=True) - res = super(ReturnPicking, self_with_context).create_returns() if not self.picking_id.partner_id: raise ValidationError( _( @@ -64,12 +127,30 @@ def create_returns(self): "'Stock Picking' from which RMAs will be created" ) ) - returned_picking = self.env["stock.picking"].browse(res["res_id"]) - vals_list = [ - move._prepare_return_rma_vals(self.picking_id) - for move in returned_picking.move_lines - ] - self.env["rma"].create(vals_list) - return res - else: - return super().create_returns() + vals_list = self._prepare_rma_vals_list() + rmas = self.env["rma"].create(vals_list) + rmas.action_confirm() + picking = rmas.reception_move_id.picking_id + picking = picking and picking[0] or picking + ctx = dict(self.env.context) + ctx.update( + { + "default_partner_id": picking.partner_id.id, + "search_default_picking_type_id": picking.picking_type_id.id, + "search_default_draft": False, + "search_default_assigned": False, + "search_default_confirmed": False, + "search_default_ready": False, + "search_default_planning_issues": False, + "search_default_available": False, + } + ) + return { + "name": _("Returned Picking"), + "view_mode": "form,tree,calendar", + "res_model": "stock.picking", + "res_id": picking.id, + "type": "ir.actions.act_window", + "context": ctx, + } + return super().create_returns() diff --git a/rma_delivery/models/rma.py b/rma_delivery/models/rma.py index f6866ca20..c1dc16e73 100644 --- a/rma_delivery/models/rma.py +++ b/rma_delivery/models/rma.py @@ -1,4 +1,5 @@ # Copyright 2022 Tecnativa - David Vidal +# Copyright 2024 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models @@ -25,13 +26,6 @@ def _get_default_carrier_id(self, company, partner): delivery_method = partner_method return delivery_method - def _prepare_returning_picking(self, picking_form, origin=None): - res = super()._prepare_returning_picking(picking_form, origin) - picking_form.carrier_id = self._get_default_carrier_id( - picking_form.company_id, picking_form.partner_id - ) - return res - def create_replace(self, scheduled_date, warehouse, product, qty, uom): existing_pickings = self.delivery_move_ids.mapped("picking_id") res = super().create_replace(scheduled_date, warehouse, product, qty, uom) @@ -41,3 +35,13 @@ def create_replace(self, scheduled_date, warehouse, product, qty, uom): picking.company_id, picking.partner_id ) return res + + def create_return(self, scheduled_date, qty=None, uom=None): + existing_pickings = self.delivery_move_ids.mapped("picking_id") + res = super().create_return(scheduled_date, qty, uom) + new_pickings = self.delivery_move_ids.mapped("picking_id") - existing_pickings + for picking in new_pickings: + picking.carrier_id = self._get_default_carrier_id( + picking.company_id, picking.partner_id + ) + return res diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py index 9047bb781..f082480d7 100644 --- a/rma_sale/models/__init__.py +++ b/rma_sale/models/__init__.py @@ -4,4 +4,3 @@ from . import res_config_settings from . import rma from . import sale -from . import stock_move diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py index 31b3254e3..418ddd690 100644 --- a/rma_sale/models/rma.py +++ b/rma_sale/models/rma.py @@ -173,4 +173,17 @@ def _prepare_refund_line(self, line_form): == 0 ): line_form.sale_line_ids.add(line) + analytic_account = line.order_id.analytic_account_id + if analytic_account: + line_form.analytic_account_id = analytic_account return res + + def _prepare_procurement_group_values(self): + values = super()._prepare_procurement_group_values() + if not self.env.context.get("ignore_rma_sale_order") and self.order_id: + values["sale_id"] = self.order_id.id + return values + + def _create_delivery_procurement_group(self): + self = self.with_context(ignore_rma_sale_order=True) + return super()._create_delivery_procurement_group() diff --git a/rma_sale/models/sale.py b/rma_sale/models/sale.py index 609ba13dc..97b6209e9 100644 --- a/rma_sale/models/sale.py +++ b/rma_sale/models/sale.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models @@ -118,10 +119,23 @@ def prepare_sale_rma_data(self): self.ensure_one() # Method helper to filter chained moves - def destination_moves(_move): - return _move.mapped("move_dest_ids").filtered( + def _get_chained_moves(_moves, done_moves=None): + moves = _moves.browse() + done_moves = done_moves or _moves.browse() + for move in _moves: + if move.location_dest_id.usage == "customer": + moves |= move.returned_move_ids + else: + moves |= move.move_dest_ids + done_moves |= _moves + moves = moves.filtered( lambda r: r.state in ["partially_available", "assigned", "done"] ) + if not moves: + return moves + moves -= done_moves + moves |= _get_chained_moves(moves, done_moves) + return moves product = self.product_id if self.product_id.type not in ["product", "consu"]: @@ -134,21 +148,13 @@ def destination_moves(_move): # to return. When a product is re-delivered it should be # allowed to open an RMA again on it. qty = move.product_uom_qty - qty_returned = 0 - move_dest = destination_moves(move) - # With the return of the return of the return we could have an - # infinite loop, so we should avoid it dropping already explored - # move_dest_ids - visited_moves = move + move_dest - while move_dest: - qty_returned -= sum(move_dest.mapped("product_uom_qty")) - move_dest = destination_moves(move_dest) - visited_moves - if move_dest: - visited_moves += move_dest - qty += sum(move_dest.mapped("product_uom_qty")) - move_dest = destination_moves(move_dest) - visited_moves + for _move in _get_chained_moves(move): + factor = 1 + if _move.location_dest_id.usage != "customer": + factor = -1 + qty += factor * _move.product_uom_qty # If by chance we get a negative qty we should ignore it - qty = max(0, sum((qty, qty_returned))) + qty = max(0, qty) data.append( { "product": move.product_id, diff --git a/rma_sale/models/stock_move.py b/rma_sale/models/stock_move.py deleted file mode 100644 index 382fcb8e3..000000000 --- a/rma_sale/models/stock_move.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2020 Tecnativa - Ernesto Tejeda -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models - - -class StockMove(models.Model): - _inherit = "stock.move" - - def _prepare_return_rma_vals(self, original_picking): - res = super()._prepare_return_rma_vals(original_picking) - res.update(order_id=original_picking.sale_id.id) - return res diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py index 45908b6d0..67ae098d9 100644 --- a/rma_sale/tests/test_rma_sale.py +++ b/rma_sale/tests/test_rma_sale.py @@ -104,7 +104,6 @@ def test_create_rma_from_so(self): rma.action_confirm() rma.reception_move_id.quantity_done = rma.product_uom_qty rma.reception_move_id.picking_id._action_done() - # Refund the RMA rma.action_refund() self.assertEqual(self.order_line.qty_delivered, 0) self.assertEqual(self.order_line.qty_invoiced, -5) diff --git a/rma_sale/wizard/__init__.py b/rma_sale/wizard/__init__.py index f2e50b2e6..d25dc1069 100644 --- a/rma_sale/wizard/__init__.py +++ b/rma_sale/wizard/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import sale_order_rma_wizard +from . import stock_picking_return diff --git a/rma_sale/wizard/stock_picking_return.py b/rma_sale/wizard/stock_picking_return.py new file mode 100644 index 000000000..f9bc5823a --- /dev/null +++ b/rma_sale/wizard/stock_picking_return.py @@ -0,0 +1,29 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ReturnPicking(models.TransientModel): + _inherit = "stock.return.picking" + + def _prepare_rma_partner_values(self): + sale_order = self.picking_id.sale_id + if not sale_order: + return super()._prepare_rma_partner_values() + return ( + sale_order.partner_id, + sale_order.partner_invoice_id, + sale_order.partner_shipping_id, + ) + + def _prepare_rma_values(self): + vals = super()._prepare_rma_values() + sale_order = self.picking_id.sale_id + if sale_order: + vals.update( + { + "order_id": sale_order.id, + } + ) + return vals