From ed3f242cb2528606b9019036ee62c7fa5c7537ee Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Thu, 24 Aug 2023 10:04:56 +0200 Subject: [PATCH] [MIG] rma: Migration to 16.0 * Standard procedure. * Transfer view groups to nodes. * Adjusted upstream changed field names. * Converted onchanges to computed writable fields. * Replace `Form` by direct dictionary vals in record creation, as they don't handle now properly multiple existing fields in the view, and computed writable improve the compatibility on new values. * Replace domain returned on onchange by static domain in field. * Change maintainer. TT44213 --- rma/README.rst | 14 +- rma/__manifest__.py | 6 +- .../15.0.1.0.0/noupdate_changes.xml | 99 ----- rma/migrations/15.0.1.0.0/post-migration.py | 18 - rma/models/res_company.py | 14 +- rma/models/rma.py | 408 +++++++++--------- rma/models/stock_picking.py | 17 +- rma/static/description/index.html | 8 +- rma/tests/test_rma.py | 121 ++---- rma/views/res_partner_views.xml | 3 +- rma/views/rma_finalization_views.xml | 2 + rma/views/rma_team_views.xml | 2 + rma/views/rma_views.xml | 31 +- rma/views/stock_picking_views.xml | 2 +- rma/views/stock_warehouse_views.xml | 7 +- rma/wizard/rma_delivery_views.xml | 2 + rma/wizard/rma_split_views.xml | 2 + rma/wizard/stock_picking_return.py | 42 +- rma/wizard/stock_picking_return_views.xml | 9 +- 19 files changed, 350 insertions(+), 457 deletions(-) delete mode 100644 rma/migrations/15.0.1.0.0/noupdate_changes.xml delete mode 100644 rma/migrations/15.0.1.0.0/post-migration.py diff --git a/rma/README.rst b/rma/README.rst index d0ef4f577..2b9fe6203 100644 --- a/rma/README.rst +++ b/rma/README.rst @@ -14,14 +14,14 @@ Return Merchandise Authorization Management :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github - :target: https://github.com/OCA/rma/tree/15.0/rma + :target: https://github.com/OCA/rma/tree/16.0/rma :alt: OCA/rma .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rma-15-0/rma-15-0-rma + :target: https://translation.odoo-community.org/projects/rma-16-0/rma-16-0-rma :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/145/15.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/rma&target_branch=16.0 + :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -138,7 +138,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -184,6 +184,6 @@ Current `maintainer `__: |maintainer-ernestotejeda| -This module is part of the `OCA/rma `_ project on GitHub. +This module is part of the `OCA/rma `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/rma/__manifest__.py b/rma/__manifest__.py index 0f46d6e63..9b3d4a1d3 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -1,14 +1,16 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2021-2023 Tecnativa - David Vidal +# Copyright 2021-2023 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Return Merchandise Authorization Management", "summary": "Return Merchandise Authorization (RMA)", - "version": "15.0.1.1.3", + "version": "16.0.1.0.0", "development_status": "Production/Stable", "category": "RMA", "website": "https://github.com/OCA/rma", "author": "Tecnativa, Odoo Community Association (OCA)", - "maintainers": ["ernestotejeda"], + "maintainers": ["pedrobaeza"], "license": "AGPL-3", "depends": ["stock_account"], "data": [ diff --git a/rma/migrations/15.0.1.0.0/noupdate_changes.xml b/rma/migrations/15.0.1.0.0/noupdate_changes.xml deleted file mode 100644 index 7937b11e7..000000000 --- a/rma/migrations/15.0.1.0.0/noupdate_changes.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - {{object.user_id.email_formatted}} - {{object.partner_id.id}} - {{object.company_id.name}} RMA (Ref {{object.name or 'n/a' }}) - {{(object.name or '')}} - {{object.partner_id.lang}} - -
-

- Dear - - - - -
-
- Here is the RMA - - - - from - - . -
-
- Do not hesitate to contact us if you have any question. -

-
-
-
- - {{object.user_id.email_formatted }} - {{object.partner_id.id}} - {{object.company_id.name}} RMA (Ref {{object.name or 'n/a' }}) products received - {{(object.name or '')}} - {{object.partner_id.lang}} - -
-

- Dear - - - - -
-
- The products for your RMA - - - - from - - have been received in our warehouse. -
-
- Do not hesitate to contact us if you have any question. -

-
-
-
- - {{object.user_id.email_formatted}} - {{object.partner_id.id}} - {{object.company_id.name}} Your RMA has been succesfully created (Ref {{object.name or 'n/a' }}) - {{(object.name or '')}} - {{object.partner_id.lang}} - -
-

- Dear - - - - -
-
- You've succesfully placed your RMA - - - - on - - . Our team will check it and will validate it as soon as possible. -
-
- Do not hesitate to contact us if you have any question. -

-
-
-
-
diff --git a/rma/migrations/15.0.1.0.0/post-migration.py b/rma/migrations/15.0.1.0.0/post-migration.py deleted file mode 100644 index d83f6aba0..000000000 --- a/rma/migrations/15.0.1.0.0/post-migration.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2022 Tecnativa - Víctor Martínez -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - openupgrade.load_data(env.cr, "rma", "migrations/15.0.1.0.0/noupdate_changes.xml") - openupgrade.delete_record_translations( - env.cr, - "rma", - [ - "mail_template_rma_notification", - "mail_template_rma_receipt_notification", - "mail_template_rma_draft_notification", - ], - ) diff --git a/rma/models/res_company.py b/rma/models/res_company.py index 94d274dfb..2b55930a3 100644 --- a/rma/models/res_company.py +++ b/rma/models/res_company.py @@ -1,10 +1,11 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models -class Company(models.Model): +class ResCompany(models.Model): _inherit = "res.company" def _default_rma_mail_confirmation_template(self): @@ -65,11 +66,12 @@ def _default_rma_mail_draft_template(self): help="Email sent to the customer when they place " "an RMA from the portal", ) - @api.model - def create(self, vals): - company = super(Company, self).create(vals) - company.create_rma_index() - return company + @api.model_create_multi + def create(self, vals_list): + companies = super().create(vals_list) + for company in companies: + company.create_rma_index() + return companies def create_rma_index(self): return ( diff --git a/rma/models/rma.py b/rma/models/rma.py index 811d2546d..f00a0724d 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import logging from collections import Counter @@ -78,6 +79,9 @@ def _domain_location_id(self): "locked": [("readonly", True)], "cancelled": [("readonly", True)], }, + compute="_compute_team_id", + store=True, + readonly=False, ) tag_ids = fields.Many2many(comodel_name="rma.tag", string="Tags") finalization_id = fields.Many2one( @@ -109,19 +113,21 @@ def _domain_location_id(self): partner_shipping_id = fields.Many2one( string="Shipping Address", comodel_name="res.partner", - readonly=True, - states={"draft": [("readonly", False)]}, help="Shipping address for current RMA.", + compute="_compute_partner_shipping_id", + store=True, + readonly=False, ) partner_invoice_id = fields.Many2one( string="Invoice Address", comodel_name="res.partner", - readonly=True, - states={"draft": [("readonly", False)]}, domain=( "['|', ('company_id', '=', False), ('company_id', '='," " company_id)]" ), help="Refund address for current RMA.", + compute="_compute_partner_invoice_id", + store=True, + readonly=False, ) commercial_partner_id = fields.Many2one( comodel_name="res.partner", @@ -149,28 +155,34 @@ def _domain_location_id(self): " ('picking_id', '!=', False)" "]" ), - readonly=True, - states={"draft": [("readonly", False)]}, + compute="_compute_move_id", + store=True, + readonly=False, ) product_id = fields.Many2one( comodel_name="product.product", domain=[("type", "in", ["consu", "product"])], + compute="_compute_product_id", + store=True, + readonly=False, ) product_uom_qty = fields.Float( string="Quantity", required=True, default=1.0, digits="Product Unit of Measure", - readonly=True, - states={"draft": [("readonly", False)]}, + compute="_compute_product_uom_qty", + store=True, + readonly=False, ) product_uom = fields.Many2one( comodel_name="uom.uom", string="UoM", required=True, - readonly=True, - states={"draft": [("readonly", False)]}, default=lambda self: self.env.ref("uom.product_uom_unit").id, + compute="_compute_product_uom", + store=True, + readonly=False, ) procurement_group_id = fields.Many2one( comodel_name="procurement.group", @@ -220,8 +232,9 @@ def _domain_location_id(self): location_id = fields.Many2one( comodel_name="stock.location", domain=_domain_location_id, - readonly=True, - states={"draft": [("readonly", False)]}, + compute="_compute_location_id", + store=True, + readonly=False, ) warehouse_id = fields.Many2one( comodel_name="stock.warehouse", @@ -463,6 +476,83 @@ def _compute_warehouse_id(self): [("rma_loc_id", "parent_of", record.location_id.id)], limit=1 ) + @api.depends("user_id") + def _compute_team_id(self): + self.team_id = False + for record in self.filtered("user_id"): + record.team_id = ( + self.env["rma.team"] + .sudo() + .search( + [ + "|", + ("user_id", "=", record.user_id.id), + ("member_ids", "=", record.user_id.id), + "|", + ("company_id", "=", False), + ("company_id", "child_of", record.company_id.ids), + ], + limit=1, + ) + ) + + @api.depends("partner_id") + def _compute_partner_shipping_id(self): + self.partner_shipping_id = False + for record in self.filtered("partner_id"): + address = record.partner_id.address_get(["delivery"]) + record.partner_shipping_id = address.get("delivery", False) + + @api.depends("partner_id") + def _compute_partner_invoice_id(self): + self.partner_invoice_id = False + for record in self.filtered("partner_id"): + address = record.partner_id.address_get(["invoice"]) + record.partner_invoice_id = address.get("invoice", False) + + @api.depends("picking_id") + def _compute_move_id(self): + """Empty move on picking change, but selecting the move in it if it's single.""" + self.move_id = False + for record in self.filtered("picking_id"): + if len(record.picking_id.move_ids) == 1: + record.move_id = record.picking_id.move_ids.id + + @api.depends("move_id") + def _compute_product_id(self): + self.product_id = False + for record in self.filtered("move_id"): + record.product_id = record.move_id.product_id.id + + @api.depends("move_id") + def _compute_product_uom_qty(self): + self.product_uom_qty = False + for record in self.filtered("move_id"): + record.product_uom_qty = record.move_id.product_uom_qty + + @api.depends("move_id", "product_id") + def _compute_product_uom(self): + for record in self: + if record.move_id: + record.product_uom = record.move_id.product_uom.id + elif record.product_id: + record.product_uom = record.product_id.uom_id + else: + record.product_uom = False + + @api.depends("picking_id", "product_id", "company_id") + def _compute_location_id(self): + for record in self: + if record.picking_id: + warehouse = record.picking_id.picking_type_id.warehouse_id + record.location_id = warehouse.rma_loc_id.id + elif not record.location_id: + company = record.company_id or self.env.company + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", company.id)], limit=1 + ) + record.location_id = warehouse.rma_loc_id.id + def _compute_access_url(self): for record in self: record.access_url = "/my/rmas/{}".format(record.id) @@ -483,76 +573,6 @@ def _check_required_after_draft(self): rma = self.filtered(lambda r: r.state not in ["draft", "cancelled"]) rma._ensure_required_fields() - # onchange methods (@api.onchange) - @api.onchange("user_id") - def _onchange_user_id(self): - if self.user_id: - self.team_id = ( - self.env["rma.team"] - .sudo() - .search( - [ - "|", - ("user_id", "=", self.user_id.id), - ("member_ids", "=", self.user_id.id), - "|", - ("company_id", "=", False), - ("company_id", "child_of", self.company_id.ids), - ], - limit=1, - ) - ) - else: - self.team_id = False - - @api.onchange("partner_id") - def _onchange_partner_id(self): - self.picking_id = False - partner_invoice_id = False - partner_shipping_id = False - if self.partner_id: - address = self.partner_id.address_get(["invoice", "delivery"]) - partner_invoice_id = address.get("invoice", False) - partner_shipping_id = address.get("delivery", False) - self.partner_invoice_id = partner_invoice_id - self.partner_shipping_id = partner_shipping_id - - @api.onchange("picking_id") - def _onchange_picking_id(self): - location = False - if self.picking_id: - warehouse = self.picking_id.picking_type_id.warehouse_id - location = warehouse.rma_loc_id.id - self.location_id = location - self.move_id = False - self.product_id = False - - @api.onchange("move_id") - def _onchange_move_id(self): - if self.move_id: - self.product_id = self.move_id.product_id - self.product_uom_qty = self.move_id.product_uom_qty - self.product_uom = self.move_id.product_uom - - @api.onchange("product_id") - def _onchange_product_id(self): - if self.product_id: - # Set UoM - if not self.product_uom or self.product_id.uom_id.id != self.product_uom.id: - self.product_uom = self.product_id.uom_id - # Set stock location (location_id) - user = self.env.user - if ( - not user.has_group("stock.group_stock_multi_locations") - and not self.location_id - ): - # If this condition is True, it is because a picking is not set - company = self.company_id or self.env.company - warehouse = self.env["stock.warehouse"].search( - [("company_id", "=", company.id)], limit=1 - ) - self.location_id = warehouse.rma_loc_id.id - # CRUD methods (ORM overrides) @api.model_create_multi def create(self, vals_list): @@ -677,41 +697,25 @@ def action_refund(self): group_dict[key] |= record for rmas in group_dict.values(): origin = ", ".join(rmas.mapped("name")) - invoice_form = Form( - self.env["account.move"] - .sudo() - .with_context( - default_move_type="out_refund", - company_id=rmas[0].company_id.id, - ), - "account.view_move_form", - ) - rmas[0]._prepare_refund(invoice_form, origin) - refund = invoice_form.save() + refund_vals = rmas[0]._prepare_refund_vals(origin) for rma in rmas: - # For each iteration the Form is edited, a new invoice line - # is added and then saved. This is to generate the other - # lines of the accounting entry and to specify the associated - # RMA to that new invoice line. - invoice_form = Form(refund) - with invoice_form.invoice_line_ids.new() as line_form: - rma._prepare_refund_line(line_form) - refund = invoice_form.save() - line = refund.invoice_line_ids.filtered(lambda r: not r.rma_id) - line.rma_id = rma.id - rma.write( - { - "refund_line_id": line.id, - "refund_id": refund.id, - "state": "refunded", - } + refund_vals["invoice_line_ids"].append( + (0, 0, rma._prepare_refund_line_vals()) ) - refund.invoice_origin = origin + refund = self.env["account.move"].sudo().create(refund_vals) refund.with_user(self.env.uid).message_post_with_view( "mail.message_origin_link", values={"self": refund, "origin": rmas}, subtype_id=self.env.ref("mail.mt_note").id, ) + for line in refund.invoice_line_ids: + line.rma_id.write( + { + "refund_line_id": line.id, + "refund_id": refund.id, + "state": "refunded", + } + ) def action_replace(self): """Invoked when 'Replace' button in rma form view is clicked.""" @@ -880,7 +884,6 @@ def _ensure_required_fields(self): rma._check_required_after_draft rma.action_confirm """ - ir_translation = self.env["ir.translation"] required = [ "partner_id", "partner_shipping_id", @@ -891,7 +894,17 @@ def _ensure_required_fields(self): for record in self: desc = "" for field in filter(lambda item: not record[item], required): - desc += "\n%s" % ir_translation.get_field_string("rma")[field] + field_record = ( + self.env["ir.model.fields"] + .sudo() + .search( + [ + ("model_id.model", "=", record._name), + ("name", "=", field), + ] + ) + ) + desc += f"\n{field_record.field_description}" if desc: raise ValidationError(_("Required field(s):%s") % desc) @@ -982,9 +995,9 @@ def _create_receptions_from_picking(self): active_model="stock.picking", ) ) - if self.location_id: - stock_return_picking_form.location_id = self.location_id return_wizard = stock_return_picking_form.save() + if self.location_id: + return_wizard.location_id = self.location_id return_wizard.product_return_moves.filtered( lambda r: r.move_id != self.move_id ).unlink() @@ -992,8 +1005,8 @@ def _create_receptions_from_picking(self): 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. + # The to_refund field is now True by default, which isn't right in the + # RMA creation context "to_refund": False, } ) @@ -1005,20 +1018,13 @@ def _create_receptions_from_picking(self): 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 = picking.move_ids 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 = self.env["stock.picking"].create(self._prepare_picking_vals()) picking.action_confirm() picking.action_assign() picking.message_post_with_view( @@ -1026,17 +1032,33 @@ def _create_receptions_from_product(self): 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 + return picking.move_ids + + def _prepare_picking_vals(self): + return { + "picking_type_id": self.warehouse_id.rma_in_type_id.id, + "origin": self.name, + "partner_id": self.partner_shipping_id.id, + "location_id": self.partner_shipping_id.property_stock_customer.id, + "location_dest_id": self.location_id.id, + "move_ids": [ + ( + 0, + 0, + { + "product_id": self.product_id.id, + # same text as origin move or product text in partner lang + "name": self.move_id.name + or self.product_id.with_context( + lang=self.partner_id.lang or "en_US" + ).display_name, + "location_id": self.partner_shipping_id.property_stock_customer.id, + "location_dest_id": self.location_id.id, + "product_uom_qty": self.product_uom_qty, + }, + ) + ], + } # Extract business methods def extract_quantity(self, qty, uom): @@ -1074,7 +1096,7 @@ def extract_quantity(self, qty, uom): return extracted_rma # Refund business methods - def _prepare_refund(self, invoice_form, origin): + def _prepare_refund_vals(self, origin=False): """Hook method for preparing the refund Form. This method could be override in order to add new custom field @@ -1084,11 +1106,16 @@ def _prepare_refund(self, invoice_form, origin): rma.action_refund """ self.ensure_one() - invoice_form.partner_id = self.partner_invoice_id - # Avoid set partner default value - invoice_form.invoice_payment_term_id = self.env["account.payment.term"] + return { + "move_type": "out_refund", + "company_id": self.company_id.id, + "partner_id": self.partner_invoice_id.id, + "invoice_payment_term_id": False, + "invoice_origin": origin, + "invoice_line_ids": [], + } - def _prepare_refund_line(self, line_form): + def _prepare_refund_line_vals(self): """Hook method for preparing a refund line Form. This method could be override in order to add new custom field @@ -1098,31 +1125,13 @@ def _prepare_refund_line(self, line_form): rma.action_refund """ self.ensure_one() - product = self._get_refund_line_product() - qty, uom = self._get_refund_line_quantity() - line_form.product_id = product - line_form.quantity = qty - line_form.product_uom_id = uom - line_form.price_unit = self._get_refund_line_price_unit() - - def _get_refund_line_product(self): - """To be overriden in a third module with the proper origin values - in case a kit is linked with the rma""" - return self.product_id - - def _get_refund_line_quantity(self): - """To be overriden in a third module with the proper origin values - in case a kit is linked with the rma""" - return (self.product_uom_qty, self.product_uom) - - def _get_refund_line_price_unit(self): - """To be overriden in a third module with the proper origin values - in case a sale order is linked to the original move""" - return self.product_id.lst_price - - def _get_extra_refund_line_vals(self): - """Override to write aditional stuff into the refund line""" - return {} + return { + "product_id": self.product_id.id, + "quantity": self.product_uom_qty, + "product_uom_id": self.product_uom.id, + "price_unit": self.product_id.lst_price, + "rma_id": self.id, + } # Returning business methods def create_return(self, scheduled_date, qty=None, uom=None): @@ -1148,32 +1157,13 @@ def create_return(self, scheduled_date, qty=None, uom=None): 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", - ) - rmas[0]._prepare_returning_picking(picking_form, origin) - picking = picking_form.save() + picking_vals = rmas[0]._prepare_returning_picking_vals(origin) 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, + picking_vals["move_ids"].append( + (0, 0, rma._prepare_returning_move_vals(scheduled_date, qty, uom)) ) - if "product_qty" in move_vals: - move_vals.pop("product_qty") - self.env["stock.move"].sudo().create(move_vals) + picking = self.env["stock.picking"].create(picking_vals) + for rma in rmas: rma.message_post( body=_( 'Return: - + Return Merchandise Authorization Management