diff --git a/rma/data/mail_data.xml b/rma/data/mail_data.xml index 175d4436..a7497726 100644 --- a/rma/data/mail_data.xml +++ b/rma/data/mail_data.xml @@ -8,7 +8,7 @@ RMA in draft state - RMA Notificatoin + RMA Notification rma RMA automatic customer notifications @@ -59,4 +59,64 @@ + + + RMA Receipt Notification + + ${object.user_id.email_formatted |safe} + ${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 ${object.partner_id.name} + % if object.partner_id.parent_id: + (${object.partner_id.parent_id.name}) + % endif +

+ The products for your RMA ${object.name} + from ${object.company_id.name} have been received in our warehouse. +

+ Do not hesitate to contact us if you have any question. +

+
+
+
+ + RMA Draft Notification + + ${object.user_id.email_formatted |safe} + ${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 ${object.partner_id.name} + % if object.partner_id.parent_id: + (${object.partner_id.parent_id.name}) + % endif +

+ You've succesfully placed your RMA ${object.name} + on ${object.company_id.name}. 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/models/res_company.py b/rma/models/res_company.py index 0a270769..479e7e7c 100644 --- a/rma/models/res_company.py +++ b/rma/models/res_company.py @@ -13,11 +13,32 @@ class Company(models.Model): except ValueError: return False + def _default_rma_mail_receipt_template(self): + try: + return self.env.ref("rma.mail_template_rma_receipt_notification").id + except ValueError: + return False + + def _default_rma_mail_draft_template(self): + try: + return self.env.ref("rma.mail_template_rma_draft_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.", ) + send_rma_receipt_confirmation = fields.Boolean( + string="Send RMA Receipt Confirmation", + help="When the RMA receipt is confirmed, send a confirmation email " + "to the customer.", + ) + send_rma_draft_confirmation = fields.Boolean( + string="Send RMA draft Confirmation", + help="When a customer places an RMA, send a notification with it", + ) rma_mail_confirmation_template_id = fields.Many2one( comodel_name="mail.template", string="Email Template confirmation for RMA", @@ -25,6 +46,20 @@ class Company(models.Model): default=_default_rma_mail_confirmation_template, help="Email sent to the customer once the RMA is confirmed.", ) + rma_mail_receipt_confirmation_template_id = fields.Many2one( + comodel_name="mail.template", + string="Email Template receipt confirmation for RMA", + domain="[('model', '=', 'rma')]", + default=_default_rma_mail_receipt_template, + help="Email sent to the customer once the RMA products are received.", + ) + rma_mail_draft_confirmation_template_id = fields.Many2one( + comodel_name="mail.template", + string="Email Template draft notification for RMA", + domain="[('model', '=', 'rma')]", + default=_default_rma_mail_draft_template, + help="Email sent to the customer when they place " "an RMA from the portal", + ) @api.model def create(self, vals): diff --git a/rma/models/res_config_settings.py b/rma/models/res_config_settings.py index d18ce044..751656bb 100644 --- a/rma/models/res_config_settings.py +++ b/rma/models/res_config_settings.py @@ -12,3 +12,15 @@ class ResConfigSettings(models.TransientModel): rma_mail_confirmation_template_id = fields.Many2one( related="company_id.rma_mail_confirmation_template_id", readonly=False, ) + send_rma_receipt_confirmation = fields.Boolean( + related="company_id.send_rma_receipt_confirmation", readonly=False, + ) + rma_mail_receipt_confirmation_template_id = fields.Many2one( + related="company_id.rma_mail_receipt_confirmation_template_id", readonly=False, + ) + send_rma_draft_confirmation = fields.Boolean( + related="company_id.send_rma_draft_confirmation", readonly=False, + ) + rma_mail_draft_confirmation_template_id = fields.Many2one( + related="company_id.rma_mail_draft_confirmation_template_id", readonly=False, + ) diff --git a/rma/models/rma.py b/rma/models/rma.py index c125f93d..4e8068a9 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -493,7 +493,13 @@ class Rma(models.Model): # Assign a default team_id which will be the first in the sequence if "team_id" not in vals: vals["team_id"] = self.env["rma.team"].search([], limit=1).id - return super().create(vals_list) + rmas = super().create(vals_list) + # Send acknowledge when the RMA is created from the portal and the + # company has the proper setting active. This context is set by the + # `rma_sale` module. + if self.env.context.get("from_portal"): + rmas._send_draft_email() + return rmas def copy(self, default=None): team = super().copy(default) @@ -511,6 +517,16 @@ class Rma(models.Model): ) return super().unlink() + def _send_draft_email(self): + """Send customer notifications they place the RMA from the portal""" + for rma in self.filtered("company_id.send_rma_draft_confirmation"): + rma_template_id = rma.company_id.rma_mail_draft_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) + def _send_confirmation_email(self): """Auto send notifications""" for rma in self.filtered(lambda p: p.company_id.send_rma_confirmation): @@ -521,6 +537,18 @@ class Rma(models.Model): default_subtype_id=self.env.ref("rma.mt_rma_notification").id, ).message_post_with_template(rma_template_id) + def _send_receipt_confirmation_email(self): + """Send customer notifications when the products are received""" + for rma in self.filtered("company_id.send_rma_receipt_confirmation"): + rma_template_id = ( + rma.company_id.rma_mail_receipt_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() @@ -1217,6 +1245,16 @@ class Rma(models.Model): return "RMA Report - %s" % self.name # Other business methods + + def update_received_state_on_reception(self): + """ Invoked by: + [stock.move]._action_done + Here we can attach methods to trigger when the customer products + are received on the RMA location, such as automatic notifications + """ + self.write({"state": "received"}) + self._send_receipt_confirmation_email() + def update_received_state(self): """ Invoked by: [stock.move].unlink diff --git a/rma/models/rma_tag.py b/rma/models/rma_tag.py index 9f96059e..db7d337b 100644 --- a/rma/models/rma_tag.py +++ b/rma/models/rma_tag.py @@ -13,6 +13,9 @@ class RmaTag(models.Model): help="The active field allows you to hide the category without " "removing it.", ) name = fields.Char(string="Tag Name", required=True, translate=True, copy=False,) + is_public = fields.Boolean( + string="Public Tag", help="The tag is visible in the portal view", + ) color = fields.Integer(string="Color Index") rma_ids = fields.Many2many(comodel_name="rma") diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py index ade29234..bb4653ea 100644 --- a/rma/models/stock_move.py +++ b/rma/models/stock_move.py @@ -68,7 +68,7 @@ class StockMove(models.Model): .mapped("rma_receiver_ids") .filtered(lambda r: r.state == "confirmed") ) - to_be_received.write({"state": "received"}) + to_be_received.update_received_state_on_reception() # Set RMAs as delivered move_done.mapped("rma_id").update_replaced_state() move_done.mapped("rma_id").update_returned_state() diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index 1f07b7be..e2f1d0e5 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -665,19 +665,50 @@ class TestRma(SavepointCase): 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( + self.company.send_rma_confirmation = True + self.company.send_rma_receipt_confirmation = True + self.company.send_rma_draft_confirmation = True + self.company.rma_mail_confirmation_template_id = self.env.ref( "rma.mail_template_rma_notification" ) + self.company.rma_mail_receipt_confirmation_template_id = self.env.ref( + "rma.mail_template_rma_receipt_notification" + ) + self.company.rma_mail_draft_confirmation_template_id = self.env.ref( + "rma.mail_template_rma_draft_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( + # Force the context to mock an RMA created from the portal, which is + # feature that we get on `rma_sale`. We drop it after the RMA creation + # to avoid uncontrolled side effects + ctx = self.env.context + self.env.context = dict(ctx, from_portal=True) + rma = self._create_rma(self.partner, self.product, 10, self.rma_loc) + self.env.context = ctx + mail_draft = 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) + rma.action_confirm() + mail_confirm = ( + self.env["mail.message"].search([("partner_ids", "in", self.partner.ids)]) + - mail_draft + ) + self.assertTrue(rma.name in mail_confirm.subject) + self.assertTrue(rma.name in mail_confirm.body) + self.assertEqual( + self.env.ref("rma.mt_rma_notification"), mail_confirm.subtype_id + ) + # Now we'll confirm the incoming goods picking and the automatic + # reception notification should be sent + rma.reception_move_id.quantity_done = rma.product_uom_qty + rma.reception_move_id.picking_id.button_validate() + mail_receipt = ( + self.env["mail.message"].search([("partner_ids", "in", self.partner.ids)]) + - mail_draft + - mail_confirm + ) + self.assertTrue(rma.name in mail_receipt.subject) + self.assertTrue("products received" in mail_receipt.subject) diff --git a/rma/views/res_config_settings_views.xml b/rma/views/res_config_settings_views.xml index 744da02b..423dff2d 100644 --- a/rma/views/res_config_settings_views.xml +++ b/rma/views/res_config_settings_views.xml @@ -46,6 +46,82 @@ +
+
+ +
+
+
+
+
+
+ +
+
+
+
diff --git a/rma/views/rma_portal_templates.xml b/rma/views/rma_portal_templates.xml index 002a5c80..a102f80d 100644 --- a/rma/views/rma_portal_templates.xml +++ b/rma/views/rma_portal_templates.xml @@ -115,6 +115,18 @@ + + + + + + + @@ -51,6 +52,7 @@ + diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py index 75752cc6..fb3af4cf 100644 --- a/rma_sale/__manifest__.py +++ b/rma_sale/__manifest__.py @@ -17,6 +17,7 @@ "views/rma_views.xml", "views/sale_views.xml", "views/sale_portal_template.xml", + "views/res_config_settings_views.xml", "wizard/sale_order_rma_wizard_views.xml", ], } diff --git a/rma_sale/controllers/sale_portal.py b/rma_sale/controllers/sale_portal.py index e8eec8e6..eb5ab385 100644 --- a/rma_sale/controllers/sale_portal.py +++ b/rma_sale/controllers/sale_portal.py @@ -30,12 +30,18 @@ class CustomerPortal(CustomerPortal): } # Set wizard line vals mapped_vals = {} + custom_vals = {} partner_shipping_id = post.pop("partner_shipping_id", False) for name, value in post.items(): - row, field_name = name.split("-", 1) - if wizard_line_field_types.get(field_name) == "many2one": - value = int(value) if value else False - mapped_vals.setdefault(row, {}).update({field_name: value}) + try: + row, field_name = name.split("-", 1) + if wizard_line_field_types.get(field_name) == "many2one": + value = int(value) if value else False + mapped_vals.setdefault(row, {}).update({field_name: value}) + # Catch possible form custom fields to add them to the RMA + # description values + except ValueError: + custom_vals.update({name: value}) # If no operation is filled, no RMA will be created line_vals = [ (0, 0, vals) for vals in mapped_vals.values() if vals.get("operation_id") @@ -43,11 +49,19 @@ class CustomerPortal(CustomerPortal): # Create wizard an generate rmas order = order_obj.browse(order_id).sudo() location_id = order.warehouse_id.rma_loc_id.id + # Add custom fields text + custom_description = "" + if custom_vals: + custom_description = r"
---
" + custom_description += r"
".join( + ["{}: {}".format(x, y) for x, y in custom_vals.items()] + ) wizard = wizard_obj.with_context(active_id=order_id).create( { "line_ids": line_vals, "location_id": location_id, "partner_shipping_id": partner_shipping_id, + "custom_description": custom_description, } ) rma = wizard.sudo().create_rma(from_portal=True) @@ -61,3 +75,27 @@ class CustomerPortal(CustomerPortal): else: route = "/my/rmas?sale_id=%d" % order_id return request.redirect(route) + + @http.route( + ["/my/requestrma/"], type="http", auth="public", website=True + ) + def request_sale_rma(self, order_id, access_token=None, **kw): + """Request RMA on a single page""" + try: + order_sudo = self._document_check_access( + "sale.order", order_id, access_token=access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + if order_sudo.state in ("draft", "sent", "cancel"): + return request.redirect("/my") + values = { + "sale_order": order_sudo, + "page_name": "request_rma", + "default_url": order_sudo.get_portal_url(), + "token": access_token, + "partner_id": order_sudo.partner_id.id, + } + if order_sudo.company_id: + values["res_company"] = order_sudo.company_id + return request.render("rma_sale.request_rma_single_page", values) diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py index 0090dd37..16972d9b 100644 --- a/rma_sale/models/__init__.py +++ b/rma_sale/models/__init__.py @@ -1,5 +1,6 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - +from . import res_company +from . import res_config_settings from . import rma from . import sale from . import stock_move diff --git a/rma_sale/models/res_company.py b/rma_sale/models/res_company.py new file mode 100644 index 00000000..021eea2c --- /dev/null +++ b/rma_sale/models/res_company.py @@ -0,0 +1,13 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + show_full_page_sale_rma = fields.Boolean( + string="Full page RMA creation", + help="From the frontend sale order page go to a single RMA page " + "creation instead of the usual popup", + ) diff --git a/rma_sale/models/res_config_settings.py b/rma_sale/models/res_config_settings.py new file mode 100644 index 00000000..dd42c753 --- /dev/null +++ b/rma_sale/models/res_config_settings.py @@ -0,0 +1,11 @@ +# 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" + + show_full_page_sale_rma = fields.Boolean( + related="company_id.show_full_page_sale_rma", readonly=False, + ) diff --git a/rma_sale/views/res_config_settings_views.xml b/rma_sale/views/res_config_settings_views.xml new file mode 100644 index 00000000..61618781 --- /dev/null +++ b/rma_sale/views/res_config_settings_views.xml @@ -0,0 +1,36 @@ + + + + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/rma_sale/views/sale_portal_template.xml b/rma_sale/views/sale_portal_template.xml index 78de3dc4..0150d4ab 100644 --- a/rma_sale/views/sale_portal_template.xml +++ b/rma_sale/views/sale_portal_template.xml @@ -1,4 +1,194 @@ + +