diff --git a/rma/__manifest__.py b/rma/__manifest__.py index aac3ec2c..9abf748e 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -29,6 +29,7 @@ "views/rma_views.xml", "views/stock_picking_views.xml", "views/stock_warehouse_views.xml", + "views/res_config_settings_views.xml", ], "post_init_hook": "post_init_hook", "application": True, diff --git a/rma/data/mail_data.xml b/rma/data/mail_data.xml index 3a0c32c2..175d4436 100644 --- a/rma/data/mail_data.xml +++ b/rma/data/mail_data.xml @@ -7,6 +7,12 @@ RMA in draft state + + RMA Notificatoin + rma + + RMA automatic customer notifications + Draft RMA @@ -16,6 +22,14 @@ team_id + + RMA Notification + 20 + rma.team + + + team_id + RMA Notification diff --git a/rma/models/__init__.py b/rma/models/__init__.py index f3c5b7de..66a957a6 100644 --- a/rma/models/__init__.py +++ b/rma/models/__init__.py @@ -5,6 +5,7 @@ from . import rma from . import rma_operation from . import rma_team from . import res_company +from . import res_config_settings from . import res_partner from . import res_users from . import stock_move diff --git a/rma/models/account_move.py b/rma/models/account_move.py index 3bd81e6c..7c8d0af8 100644 --- a/rma/models/account_move.py +++ b/rma/models/account_move.py @@ -9,14 +9,13 @@ from odoo.tools import float_compare class AccountMove(models.Model): _inherit = "account.move" - def post(self): - """ Avoids to validate a refund with less quantity of product than - quantity in the linked RMA. - """ + def _check_rma_invoice_lines_qty(self): + """We can't refund a different qty than the stated in the RMA. + Extend to change criteria """ precision = self.env["decimal.precision"].precision_get( "Product Unit of Measure" ) - if ( + return ( self.sudo() .mapped("invoice_line_ids") .filtered( @@ -26,7 +25,13 @@ class AccountMove(models.Model): < 0 ) ) - ): + ) + + def post(self): + """ Avoids to validate a refund with less quantity of product than + quantity in the linked RMA. + """ + if self._check_rma_invoice_lines_qty(): raise ValidationError( _( "There is at least one invoice lines whose quantity is " diff --git a/rma/models/res_company.py b/rma/models/res_company.py index a7129391..0a270769 100644 --- a/rma/models/res_company.py +++ b/rma/models/res_company.py @@ -1,12 +1,31 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import _, api, models +from odoo import _, api, fields, models class Company(models.Model): _inherit = "res.company" + def _default_rma_mail_confirmation_template(self): + try: + return self.env.ref("rma.mail_template_rma_notification").id + except ValueError: + return False + + send_rma_confirmation = fields.Boolean( + string="Send RMA Confirmation", + help="When the delivery is confirmed, send a confirmation email " + "to the customer.", + ) + rma_mail_confirmation_template_id = fields.Many2one( + comodel_name="mail.template", + string="Email Template confirmation for RMA", + domain="[('model', '=', 'rma')]", + default=_default_rma_mail_confirmation_template, + help="Email sent to the customer once the RMA is confirmed.", + ) + @api.model def create(self, vals): company = super(Company, self).create(vals) diff --git a/rma/models/res_config_settings.py b/rma/models/res_config_settings.py new file mode 100644 index 00000000..d18ce044 --- /dev/null +++ b/rma/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + send_rma_confirmation = fields.Boolean( + related="company_id.send_rma_confirmation", readonly=False, + ) + rma_mail_confirmation_template_id = fields.Many2one( + related="company_id.rma_mail_confirmation_template_id", readonly=False, + ) diff --git a/rma/models/rma.py b/rma/models/rma.py index 8e7ddf4c..e2bf8fec 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -73,6 +73,13 @@ class Rma(models.Model): index=True, tracking=True, ) + partner_shipping_id = fields.Many2one( + string="Shipping Address", + comodel_name="res.partner", + readonly=True, + states={"draft": [("readonly", False)]}, + help="Shipping address for current RMA.", + ) partner_invoice_id = fields.Many2one( string="Invoice Address", comodel_name="res.partner", @@ -385,7 +392,9 @@ class Rma(models.Model): record.access_url = "/my/rmas/{}".format(record.id) # Constrains methods (@api.constrains) - @api.constrains("state", "partner_id", "partner_invoice_id", "product_id") + @api.constrains( + "state", "partner_id", "partner_shipping_id", "partner_invoice_id", "product_id" + ) def _check_required_after_draft(self): """ Check that RMAs are being created or edited with the necessary fields filled out. Only applies to 'Draft' and @@ -420,10 +429,13 @@ class Rma(models.Model): 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"]) + 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): @@ -498,13 +510,25 @@ class Rma(models.Model): ) return super().unlink() + def _send_confirmation_email(self): + """Auto send notifications""" + for rma in self.filtered(lambda p: p.company_id.send_rma_confirmation): + rma_template_id = rma.company_id.rma_mail_confirmation_template_id.id + rma.with_context( + force_send=True, + mark_rma_as_sent=True, + default_subtype_id=self.env.ref("rma.mt_rma_notification").id, + ).message_post_with_template(rma_template_id) + # Action methods def action_rma_send(self): self.ensure_one() template = self.env.ref("rma.mail_template_rma_notification", False) + template = self.company_id.rma_mail_confirmation_template_id or template form = self.env.ref("mail.email_compose_message_wizard_form", False) ctx = { "default_model": "rma", + "default_subtype_id": self.env.ref("rma.mt_rma_notification").id, "default_res_id": self.ids[0], "default_use_template": bool(template), "default_template_id": template and template.id or False, @@ -536,6 +560,7 @@ class Rma(models.Model): self.write({"reception_move_id": reception_move.id, "state": "confirmed"}) if self.partner_id not in self.message_partner_ids: self.message_subscribe([self.partner_id.id]) + self._send_confirmation_email() def action_refund(self): """Invoked when 'Refund' button in rma form view is clicked @@ -710,7 +735,10 @@ class Rma(models.Model): # Validation business methods def _ensure_required_fields(self): """ This method is used to ensure the following fields are not empty: - ['partner_id', 'partner_invoice_id', 'product_id', 'location_id'] + [ + 'partner_id', 'partner_invoice_id', 'partner_shipping_id', + 'product_id', 'location_id' + ] This method is intended to be called on confirm RMA action and is invoked by: @@ -718,7 +746,13 @@ class Rma(models.Model): rma.action_confirm """ ir_translation = self.env["ir.translation"] - required = ["partner_id", "partner_invoice_id", "product_id", "location_id"] + required = [ + "partner_id", + "partner_shipping_id", + "partner_invoice_id", + "product_id", + "location_id", + ] for record in self: desc = "" for field in filter(lambda item: not record[item], required): @@ -850,7 +884,8 @@ class Rma(models.Model): def _prepare_picking(self, picking_form): picking_form.origin = self.name - picking_form.partner_id = self.partner_id + 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 @@ -915,16 +950,32 @@ class Rma(models.Model): rma.action_refund """ self.ensure_one() - line_form.product_id = self.product_id - line_form.quantity = self.product_uom_qty - line_form.product_uom_id = self.product_uom + 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 {} + # Returning business methods def create_return(self, scheduled_date, qty=None, uom=None): """Intended to be invoked by the delivery wizard""" @@ -933,7 +984,11 @@ class Rma(models.Model): group_dict = {} rmas_to_return = self.filtered("can_be_returned") for record in rmas_to_return: - key = (record.partner_id.id, record.company_id.id, record.warehouse_id) + key = ( + record.partner_shipping_id.id, + record.company_id.id, + record.warehouse_id, + ) group_dict.setdefault(key, self.env["rma"]) group_dict[key] |= record for rmas in group_dict.values(): @@ -981,7 +1036,7 @@ class Rma(models.Model): 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_id + picking_form.partner_id = self.partner_shipping_id def _prepare_returning_move( self, move_form, scheduled_date, quantity=None, uom=None @@ -1047,7 +1102,7 @@ class Rma(models.Model): { "name": self.name, "move_type": "direct", - "partner_id": self.partner_id.id, + "partner_id": self.partner_shipping_id.id, } ) .id @@ -1059,7 +1114,7 @@ class Rma(models.Model): product, qty, uom, - self.partner_id.property_stock_customer, + self.partner_shipping_id.property_stock_customer, self.product_id.display_name, self.procurement_group_id.name, self.company_id, @@ -1077,18 +1132,27 @@ class Rma(models.Model): "group_id": group_id, "date_planned": scheduled_date, "warehouse_id": warehouse, - "partner_id": self.partner_id.id, + "partner_id": self.partner_shipping_id.id, "rma_id": self.id, "priority": self.priority, } # Mail business methods def _creation_subtype(self): - if self.state in ("draft", "confirmed"): + if self.state in ("draft"): return self.env.ref("rma.mt_rma_draft") else: return super()._creation_subtype() + def _track_subtype(self, init_values): + self.ensure_one() + if "state" in init_values: + if self.state == "draft": + return self.env.ref("rma.mt_rma_draft") + elif self.state == "confirmed": + return self.env.ref("rma.mt_rma_notification") + return super()._track_subtype(init_values) + def message_new(self, msg_dict, custom_values=None): """Extract the needed values from an incoming rma emails data-set to be used to create an RMA. diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py index a1b702c8..ade29234 100644 --- a/rma/models/stock_move.py +++ b/rma/models/stock_move.py @@ -96,13 +96,16 @@ class StockMove(models.Model): 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_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, diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index b917f269..65d4a3f1 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -44,6 +44,13 @@ class TestRma(SavepointCase): "type": "invoice", } ) + cls.partner_shipping = cls.res_partner.create( + { + "name": "Partner shipping test", + "parent_id": cls.partner.id, + "type": "delivery", + } + ) def _create_rma(self, partner=None, product=None, qty=None, location=None): rma_form = Form(self.env["rma"]) @@ -181,7 +188,8 @@ class TestRma(SavepointCase): rma.action_confirm() self.assertEqual( e.exception.name, - "Required field(s):\nCustomer\nInvoice Address\nProduct\nLocation", + "Required field(s):\nCustomer\nShipping Address\nInvoice Address\n" + "Product\nLocation", ) with Form(rma) as rma_form: rma_form.partner_id = self.partner @@ -531,7 +539,7 @@ class TestRma(SavepointCase): # One picking per partner self.assertNotEqual(pick_1.partner_id, pick_2.partner_id) self.assertEqual( - pick_1.partner_id, (rma_1 | rma_2 | rma_3).mapped("partner_id"), + pick_1.partner_id, (rma_1 | rma_2 | rma_3).mapped("partner_shipping_id"), ) self.assertEqual(pick_2.partner_id, rma_4.partner_id) # Each RMA of (rma_1, rma_2 and rma_3) is linked to a different @@ -656,3 +664,21 @@ class TestRma(SavepointCase): def test_quantities_on_hand(self): rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc) self.assertEqual(rma.product_id.qty_available, 0) + + def test_autoconfirm_email(self): + rma = self._create_rma(self.partner, self.product, 10, self.rma_loc) + rma.company_id.send_rma_confirmation = True + rma.company_id.rma_mail_confirmation_template_id = self.env.ref( + "rma.mail_template_rma_notification" + ) + previous_mails = self.env["mail.mail"].search( + [("partner_ids", "in", self.partner.ids)] + ) + self.assertFalse(previous_mails) + rma.action_confirm() + mail = self.env["mail.message"].search( + [("partner_ids", "in", self.partner.ids)] + ) + self.assertTrue(rma.name in mail.subject) + self.assertTrue(rma.name in mail.body) + self.assertEqual(self.env.ref("rma.mt_rma_notification"), mail.subtype_id) diff --git a/rma/views/res_config_settings_views.xml b/rma/views/res_config_settings_views.xml new file mode 100644 index 00000000..744da02b --- /dev/null +++ b/rma/views/res_config_settings_views.xml @@ -0,0 +1,52 @@ + + + + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/rma/views/rma_portal_templates.xml b/rma/views/rma_portal_templates.xml index ef0ea47d..002a5c80 100644 --- a/rma/views/rma_portal_templates.xml +++ b/rma/views/rma_portal_templates.xml @@ -46,7 +46,7 @@ RMA # Date - Product + Product Quantity Status @@ -66,7 +66,7 @@ - + diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml index f270b8da..5ac50e05 100644 --- a/rma/views/rma_views.xml +++ b/rma/views/rma_views.xml @@ -239,6 +239,7 @@ context="{'search_default_customer':1, 'show_address': 1, 'show_vat': True}" options="{'always_reload': True}" /> + RMA rma tree,form,pivot,calendar,activity - {"search_default_user_id": uid} + {}

Click to add a new RMA. diff --git a/rma_sale/controllers/sale_portal.py b/rma_sale/controllers/sale_portal.py index 70403a80..b587abcc 100644 --- a/rma_sale/controllers/sale_portal.py +++ b/rma_sale/controllers/sale_portal.py @@ -27,6 +27,7 @@ class CustomerPortal(CustomerPortal): wizard_obj = request.env["sale.order.rma.wizard"] # Set wizard line vals mapped_vals = {} + partner_shipping_id = post.pop("partner_shipping_id", False) for name, value in post.items(): row, field_name = name.split("-", 1) mapped_vals.setdefault(row, {}).update({field_name: value}) @@ -38,9 +39,13 @@ class CustomerPortal(CustomerPortal): order = order_obj.browse(order_id).sudo() location_id = order.warehouse_id.rma_loc_id.id wizard = wizard_obj.with_context(active_id=order_id).create( - {"line_ids": line_vals, "location_id": location_id} + { + "line_ids": line_vals, + "location_id": location_id, + "partner_shipping_id": partner_shipping_id, + } ) - rma = wizard.sudo().create_rma() + rma = wizard.sudo().create_rma(from_portal=True) for rec in rma: rec.origin += _(" (Portal)") # Add the user as follower of the created RMAs so they can diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py index 4fc540c1..70c0dfcd 100644 --- a/rma_sale/models/rma.py +++ b/rma_sale/models/rma.py @@ -25,6 +25,7 @@ class Rma(models.Model): comodel_name="sale.order.line", compute="_compute_allowed_move_ids", ) move_id = fields.Many2one(domain="[('id', 'in', allowed_move_ids)]") + sale_line_id = fields.Many2one(related="move_id.sale_line_id",) allowed_product_ids = fields.Many2many( comodel_name="product.product", compute="_compute_allowed_product_ids", ) @@ -83,3 +84,24 @@ class Rma(models.Model): if self.order_id: invoice_form.invoice_user_id = self.order_id.user_id return res + + def _get_refund_line_price_unit(self): + """Get the sale order price unit""" + if self.sale_line_id: + return self.sale_line_id.price_unit + return super()._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 """ + if not self.sale_line_id: + return super()._get_refund_line_product() + return self.sale_line_id.product_id + + def _prepare_refund_line(self, line_form): + """Add line data""" + super()._prepare_refund_line(line_form) + line = self.sale_line_id + if line: + line_form.discount = line.discount + line_form.sequence = line.sequence diff --git a/rma_sale/models/sale.py b/rma_sale/models/sale.py index bfc14243..fa91e86c 100644 --- a/rma_sale/models/sale.py +++ b/rma_sale/models/sale.py @@ -1,7 +1,7 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import _, fields, models +from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -22,6 +22,16 @@ class SaleOrder(models.Model): for record in self: record.rma_count = mapped_data.get(record.id, 0) + def _prepare_rma_wizard_line_vals(self, data): + """So we can extend the wizard easily""" + return { + "product_id": data["product"].id, + "quantity": data["quantity"], + "sale_line_id": data["sale_line_id"].id, + "uom_id": data["uom"].id, + "picking_id": data["picking"] and data["picking"].id, + } + def action_create_rma(self): self.ensure_one() if self.state not in ["sale", "done"]: @@ -30,17 +40,7 @@ class SaleOrder(models.Model): ) wizard_obj = self.env["sale.order.rma.wizard"] line_vals = [ - ( - 0, - 0, - { - "product_id": data["product"].id, - "quantity": data["quantity"], - "sale_line_id": data["sale_line_id"].id, - "uom_id": data["uom"].id, - "picking_id": data["picking"] and data["picking"].id, - }, - ) + (0, 0, self._prepare_rma_wizard_line_vals(data)) for data in self.get_delivery_rma_data() ] wizard = wizard_obj.with_context(active_id=self.id).create( @@ -76,6 +76,19 @@ class SaleOrder(models.Model): data += line.prepare_sale_rma_data() return data + @api.depends("rma_ids.refund_id") + def _get_invoiced(self): + """Search for possible RMA refunds and link them to the order. We + don't want to link their sale lines as that would unbalance the + qtys to invoice wich isn't correct for this case""" + super()._get_invoiced() + for order in self: + refunds = order.sudo().rma_ids.mapped("refund_id") + if not refunds: + continue + order.invoice_ids += refunds + order.invoice_count = len(order.invoice_ids) + class SaleOrderLine(models.Model): _inherit = "sale.order.line" @@ -98,7 +111,7 @@ class SaleOrderLine(models.Model): def prepare_sale_rma_data(self): self.ensure_one() product = self.product_id - if self.product_id.type != "product": + if self.product_id.type not in ["product", "consu"]: return {} moves = self.get_delivery_move() data = [] diff --git a/rma_sale/static/src/js/rma_portal_form.js b/rma_sale/static/src/js/rma_portal_form.js new file mode 100644 index 00000000..e904d44a --- /dev/null +++ b/rma_sale/static/src/js/rma_portal_form.js @@ -0,0 +1,45 @@ +/* Copyright 2021 Tecnativa - David Vidal + License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +odoo.define("rma_sale.animation", function(require) { + "use strict"; + + var sAnimation = require("website.content.snippets.animation"); + + // In the customer portal when a RMA operation is selected show the comments + // selector so the user doesn't miss the chance to add his comments + sAnimation.registry.rma_operation_portal = sAnimation.Class.extend({ + selector: ".rma-operation", + start: function() { + this.id = this.el.name.replace("-operation_id", ""); + this.$comment = $("#comment-" + this.id); + this.$comment_input = $("[name='" + this.id + "-description']"); + var _this = this; + this.$el.on("change", function() { + _this._onChangeOperationId(); + }); + }, + _show_comment: function() { + if (this.$comment) { + this.$comment.removeClass("show"); + this.$comment.addClass("show"); + if (this.$comment_input) { + this.$comment_input.focus(); + } + } + }, + _hide_comment: function() { + if (this.$comment) { + this.$comment.removeClass("show"); + } + }, + _onChangeOperationId: function() { + // Toggle comment on or off if an operation is requested + if (this.$el && this.$el.val()) { + this._show_comment(); + } else { + this._hide_comment(); + } + }, + }); +}); diff --git a/rma_sale/views/assets.xml b/rma_sale/views/assets.xml index e946398f..53633488 100644 --- a/rma_sale/views/assets.xml +++ b/rma_sale/views/assets.xml @@ -2,6 +2,10 @@