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
+
+
+
+
+
+
+
+
+
+
+
+ When the RMA is confirmed, send an automatic information email.
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
|
|
| |