mirror of
https://github.com/OCA/rma.git
synced 2025-02-16 17:11:47 +02:00
Sometimes there's no choice to return, refund or replace an RMA for different reasons. For example, when the customer doesn't want to repair the product because that would be too expensive. We still want finish the RMA and document the reasons. This improvement allows it. TT34164
1377 lines
50 KiB
Python
1377 lines
50 KiB
Python
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
|
|
from collections import Counter
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import AccessError, ValidationError
|
|
from odoo.tests import Form
|
|
from odoo.tools import html2plaintext
|
|
|
|
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
|
|
|
|
|
class Rma(models.Model):
|
|
_name = "rma"
|
|
_description = "RMA"
|
|
_order = "date desc, priority"
|
|
_inherit = ["mail.thread", "portal.mixin", "mail.activity.mixin"]
|
|
|
|
def _domain_location_id(self):
|
|
# this is done with sudo, intercompany rules are not applied by default so we
|
|
# add company in domain explicitly to avoid multi-company rule error when
|
|
# the user will try to choose a location
|
|
rma_loc = (
|
|
self.env["stock.warehouse"]
|
|
.search([("company_id", "in", self.env.companies.ids)])
|
|
.mapped("rma_loc_id")
|
|
)
|
|
return [("id", "child_of", rma_loc.ids)]
|
|
|
|
# General fields
|
|
sent = fields.Boolean()
|
|
name = fields.Char(
|
|
string="Name",
|
|
index=True,
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
copy=False,
|
|
default=lambda self: _("New"),
|
|
)
|
|
origin = fields.Char(
|
|
string="Source Document",
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
help="Reference of the document that generated this RMA.",
|
|
)
|
|
date = fields.Datetime(
|
|
default=lambda self: fields.Datetime.now(),
|
|
index=True,
|
|
required=True,
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
deadline = fields.Date(
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
)
|
|
user_id = fields.Many2one(
|
|
comodel_name="res.users",
|
|
string="Responsible",
|
|
index=True,
|
|
tracking=True,
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
)
|
|
team_id = fields.Many2one(
|
|
comodel_name="rma.team",
|
|
string="RMA team",
|
|
index=True,
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
)
|
|
tag_ids = fields.Many2many(comodel_name="rma.tag", string="Tags")
|
|
finalization_id = fields.Many2one(
|
|
string="Finalization Reason",
|
|
comodel_name="rma.finalization",
|
|
copy=False,
|
|
readonly=True,
|
|
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
tracking=True,
|
|
)
|
|
company_id = fields.Many2one(
|
|
comodel_name="res.company",
|
|
default=lambda self: self.env.company,
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
)
|
|
partner_id = fields.Many2one(
|
|
string="Customer",
|
|
comodel_name="res.partner",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
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",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
help="Refund address for current RMA.",
|
|
)
|
|
commercial_partner_id = fields.Many2one(
|
|
comodel_name="res.partner",
|
|
related="partner_id.commercial_partner_id",
|
|
)
|
|
picking_id = fields.Many2one(
|
|
comodel_name="stock.picking",
|
|
string="Origin Delivery",
|
|
domain="["
|
|
" ('state', '=', 'done'),"
|
|
" ('picking_type_id.code', '=', 'outgoing'),"
|
|
" ('partner_id', 'child_of', commercial_partner_id),"
|
|
"]",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
move_id = fields.Many2one(
|
|
comodel_name="stock.move",
|
|
string="Origin move",
|
|
domain="["
|
|
" ('picking_id', '=', picking_id),"
|
|
" ('picking_id', '!=', False)"
|
|
"]",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
product_id = fields.Many2one(
|
|
comodel_name="product.product",
|
|
domain=[("type", "in", ["consu", "product"])],
|
|
)
|
|
product_uom_qty = fields.Float(
|
|
string="Quantity",
|
|
required=True,
|
|
default=1.0,
|
|
digits="Product Unit of Measure",
|
|
readonly=True,
|
|
states={"draft": [("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,
|
|
)
|
|
procurement_group_id = fields.Many2one(
|
|
comodel_name="procurement.group",
|
|
string="Procurement group",
|
|
readonly=True,
|
|
states={
|
|
"draft": [("readonly", False)],
|
|
"confirmed": [("readonly", False)],
|
|
"received": [("readonly", False)],
|
|
},
|
|
)
|
|
priority = fields.Selection(
|
|
string="Priority",
|
|
selection=PROCUREMENT_PRIORITIES,
|
|
default="1",
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
operation_id = fields.Many2one(
|
|
comodel_name="rma.operation",
|
|
string="Requested operation",
|
|
)
|
|
state = fields.Selection(
|
|
[
|
|
("draft", "Draft"),
|
|
("confirmed", "Confirmed"),
|
|
("received", "Received"),
|
|
("waiting_return", "Waiting for return"),
|
|
("waiting_replacement", "Waiting for replacement"),
|
|
("refunded", "Refunded"),
|
|
("returned", "Returned"),
|
|
("replaced", "Replaced"),
|
|
("finished", "Finished"),
|
|
("locked", "Locked"),
|
|
("cancelled", "Canceled"),
|
|
],
|
|
default="draft",
|
|
copy=False,
|
|
tracking=True,
|
|
)
|
|
description = fields.Html(
|
|
states={"locked": [("readonly", True)], "cancelled": [("readonly", True)]},
|
|
)
|
|
# Reception fields
|
|
location_id = fields.Many2one(
|
|
comodel_name="stock.location",
|
|
domain=_domain_location_id,
|
|
readonly=True,
|
|
states={"draft": [("readonly", False)]},
|
|
)
|
|
warehouse_id = fields.Many2one(
|
|
comodel_name="stock.warehouse",
|
|
compute="_compute_warehouse_id",
|
|
store=True,
|
|
)
|
|
reception_move_id = fields.Many2one(
|
|
comodel_name="stock.move",
|
|
string="Reception move",
|
|
copy=False,
|
|
)
|
|
# Refund fields
|
|
refund_id = fields.Many2one(
|
|
comodel_name="account.move",
|
|
string="Refund",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
refund_line_id = fields.Many2one(
|
|
comodel_name="account.move.line",
|
|
string="Refund line",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
can_be_refunded = fields.Boolean(compute="_compute_can_be_refunded")
|
|
# Delivery fields
|
|
delivery_move_ids = fields.One2many(
|
|
comodel_name="stock.move",
|
|
inverse_name="rma_id",
|
|
string="Delivery reservation",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
delivery_picking_count = fields.Integer(
|
|
string="Delivery count",
|
|
compute="_compute_delivery_picking_count",
|
|
)
|
|
delivered_qty = fields.Float(
|
|
string="Delivered qty",
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_delivered_qty",
|
|
store=True,
|
|
)
|
|
delivered_qty_done = fields.Float(
|
|
string="Delivered qty done",
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_delivered_qty",
|
|
compute_sudo=True,
|
|
)
|
|
can_be_returned = fields.Boolean(
|
|
compute="_compute_can_be_returned",
|
|
)
|
|
can_be_replaced = fields.Boolean(
|
|
compute="_compute_can_be_replaced",
|
|
)
|
|
can_be_locked = fields.Boolean(
|
|
compute="_compute_can_be_locked",
|
|
)
|
|
can_be_finished = fields.Boolean(
|
|
compute="_compute_can_be_finished",
|
|
)
|
|
remaining_qty = fields.Float(
|
|
string="Remaining delivered qty",
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_remaining_qty",
|
|
)
|
|
remaining_qty_to_done = fields.Float(
|
|
string="Remaining delivered qty to done",
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_remaining_qty",
|
|
)
|
|
uom_category_id = fields.Many2one(
|
|
related="product_id.uom_id.category_id", string="Category UoM"
|
|
)
|
|
# Split fields
|
|
can_be_split = fields.Boolean(
|
|
compute="_compute_can_be_split",
|
|
)
|
|
origin_split_rma_id = fields.Many2one(
|
|
comodel_name="rma",
|
|
string="Extracted from",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
|
|
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)
|
|
|
|
@api.depends(
|
|
"delivery_move_ids",
|
|
"delivery_move_ids.state",
|
|
"delivery_move_ids.scrapped",
|
|
"delivery_move_ids.product_uom_qty",
|
|
"delivery_move_ids.reserved_availability",
|
|
"delivery_move_ids.quantity_done",
|
|
"delivery_move_ids.product_uom",
|
|
"product_uom",
|
|
)
|
|
def _compute_delivered_qty(self):
|
|
"""Compute 'delivered_qty' and 'delivered_qty_done' fields.
|
|
|
|
delivered_qty: represents the quantity delivery or to be
|
|
delivery. For each move in delivery_move_ids the quantity done
|
|
is taken, if it is empty the reserved quantity is taken,
|
|
otherwise the initial demand is taken.
|
|
|
|
delivered_qty_done: represents the quantity delivered and done.
|
|
For each 'done' move in delivery_move_ids the quantity done is
|
|
taken. This field is used to control when the RMA cam be set
|
|
to 'delivered' state.
|
|
"""
|
|
for record in self:
|
|
delivered_qty = 0.0
|
|
delivered_qty_done = 0.0
|
|
for move in record.delivery_move_ids.filtered(
|
|
lambda r: r.state != "cancel" and not r.scrapped
|
|
):
|
|
if move.quantity_done:
|
|
quantity_done = move.product_uom._compute_quantity(
|
|
move.quantity_done, record.product_uom
|
|
)
|
|
if move.state == "done":
|
|
delivered_qty_done += quantity_done
|
|
delivered_qty += quantity_done
|
|
elif move.reserved_availability:
|
|
delivered_qty += move.product_uom._compute_quantity(
|
|
move.reserved_availability, record.product_uom
|
|
)
|
|
elif move.product_uom_qty:
|
|
delivered_qty += move.product_uom._compute_quantity(
|
|
move.product_uom_qty, record.product_uom
|
|
)
|
|
record.delivered_qty = delivered_qty
|
|
record.delivered_qty_done = delivered_qty_done
|
|
|
|
@api.depends("product_uom_qty", "delivered_qty", "delivered_qty_done")
|
|
def _compute_remaining_qty(self):
|
|
"""Compute 'remaining_qty' and 'remaining_qty_to_done' fields.
|
|
|
|
remaining_qty: is used to set a default quantity of replacing
|
|
or returning of product to the customer.
|
|
|
|
remaining_qty_to_done: the aim of this field to control when the
|
|
RMA cam be set to 'delivered' state. An RMA with
|
|
remaining_qty_to_done <= 0 can be set to 'delivery'. It is used
|
|
in stock.move._action_done method of stock.move and
|
|
rma.extract_quantity.
|
|
"""
|
|
for r in self:
|
|
r.remaining_qty = r.product_uom_qty - r.delivered_qty
|
|
r.remaining_qty_to_done = r.product_uom_qty - r.delivered_qty_done
|
|
|
|
@api.depends(
|
|
"state",
|
|
)
|
|
def _compute_can_be_refunded(self):
|
|
"""Compute 'can_be_refunded'. This field controls the visibility
|
|
of 'Refund' button in the rma form view and determinates if
|
|
an rma can be refunded. It is used in rma.action_refund method.
|
|
"""
|
|
for record in self:
|
|
record.can_be_refunded = record.state == "received"
|
|
|
|
@api.depends("remaining_qty", "state")
|
|
def _compute_can_be_returned(self):
|
|
"""Compute 'can_be_returned'. This field controls the visibility
|
|
of the 'Return to customer' button in the rma form
|
|
view and determinates if an rma can be returned to the customer.
|
|
This field is used in:
|
|
rma._compute_can_be_split
|
|
rma._ensure_can_be_returned.
|
|
"""
|
|
for r in self:
|
|
r.can_be_returned = (
|
|
r.state in ["received", "waiting_return"] and r.remaining_qty > 0
|
|
)
|
|
|
|
@api.depends("state")
|
|
def _compute_can_be_replaced(self):
|
|
"""Compute 'can_be_replaced'. This field controls the visibility
|
|
of 'Replace' button in the rma form
|
|
view and determinates if an rma can be replaced.
|
|
This field is used in:
|
|
rma._compute_can_be_split
|
|
rma._ensure_can_be_replaced.
|
|
"""
|
|
for r in self:
|
|
r.can_be_replaced = r.state in [
|
|
"received",
|
|
"waiting_replacement",
|
|
"replaced",
|
|
]
|
|
|
|
@api.depends("state", "remaining_qty")
|
|
def _compute_can_be_finished(self):
|
|
for rma in self:
|
|
rma.can_be_finished = (
|
|
rma.state in {"received", "waiting_replacement", "waiting_return"}
|
|
and rma.remaining_qty > 0
|
|
)
|
|
|
|
@api.depends("product_uom_qty", "state", "remaining_qty", "remaining_qty_to_done")
|
|
def _compute_can_be_split(self):
|
|
"""Compute 'can_be_split'. This field controls the
|
|
visibility of 'Split' button in the rma form view and
|
|
determinates if an rma can be split.
|
|
This field is used in:
|
|
rma._ensure_can_be_split
|
|
"""
|
|
for r in self:
|
|
if r.product_uom_qty > 1 and (
|
|
(r.state == "waiting_return" and r.remaining_qty > 0)
|
|
or (r.state == "waiting_replacement" and r.remaining_qty_to_done > 0)
|
|
):
|
|
r.can_be_split = True
|
|
else:
|
|
r.can_be_split = False
|
|
|
|
@api.depends("remaining_qty_to_done", "state")
|
|
def _compute_can_be_locked(self):
|
|
for r in self:
|
|
r.can_be_locked = r.remaining_qty_to_done > 0 and r.state in [
|
|
"received",
|
|
"waiting_return",
|
|
"waiting_replacement",
|
|
]
|
|
|
|
@api.depends("location_id")
|
|
def _compute_warehouse_id(self):
|
|
for record in self.filtered("location_id"):
|
|
record.warehouse_id = self.env["stock.warehouse"].search(
|
|
[("rma_loc_id", "parent_of", record.location_id.id)], limit=1
|
|
)
|
|
|
|
def _compute_access_url(self):
|
|
for record in self:
|
|
record.access_url = "/my/rmas/{}".format(record.id)
|
|
|
|
# Constrains methods (@api.constrains)
|
|
@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
|
|
'Cancelled' states.
|
|
"""
|
|
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):
|
|
for vals in vals_list:
|
|
if vals.get("name", _("New")) == _("New"):
|
|
ir_sequence = self.env["ir.sequence"]
|
|
if "company_id" in vals:
|
|
ir_sequence = ir_sequence.with_company(vals["company_id"])
|
|
vals["name"] = ir_sequence.next_by_code("rma")
|
|
# Assign a default team_id which will be the first in the sequence
|
|
if not vals.get("team_id"):
|
|
vals["team_id"] = self.env["rma.team"].search([], limit=1).id
|
|
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)
|
|
for follower in self.message_follower_ids:
|
|
team.message_subscribe(
|
|
partner_ids=follower.partner_id.ids,
|
|
subtype_ids=follower.subtype_ids.ids,
|
|
)
|
|
return team
|
|
|
|
def unlink(self):
|
|
if self.filtered(lambda r: r.state != "draft"):
|
|
raise ValidationError(
|
|
_("You cannot delete RMAs that are not in draft state")
|
|
)
|
|
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):
|
|
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)
|
|
|
|
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()
|
|
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,
|
|
"default_composition_mode": "comment",
|
|
"mark_rma_as_sent": True,
|
|
"model_description": "RMA",
|
|
"force_email": True,
|
|
}
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"view_type": "form",
|
|
"view_mode": "form",
|
|
"res_model": "mail.compose.message",
|
|
"views": [(form.id, "form")],
|
|
"view_id": form.id,
|
|
"target": "new",
|
|
"context": ctx,
|
|
}
|
|
|
|
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"})
|
|
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
|
|
and 'rma_refund_action_server' server action is run.
|
|
"""
|
|
group_dict = {}
|
|
for record in self.filtered("can_be_refunded"):
|
|
key = (record.partner_invoice_id.id, record.company_id.id)
|
|
group_dict.setdefault(key, self.env["rma"])
|
|
group_dict[key] |= record
|
|
for rmas in group_dict.values():
|
|
origin = ", ".join(rmas.mapped("name"))
|
|
invoice_form = Form(
|
|
self.env["account.move"].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()
|
|
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.invoice_origin = origin
|
|
refund.message_post_with_view(
|
|
"mail.message_origin_link",
|
|
values={"self": refund, "origin": rmas},
|
|
subtype_id=self.env.ref("mail.mt_note").id,
|
|
)
|
|
|
|
def action_replace(self):
|
|
"""Invoked when 'Replace' button in rma form view is clicked."""
|
|
self.ensure_one()
|
|
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")
|
|
.with_context(active_id=self.id)
|
|
.read()[0]
|
|
)
|
|
action["name"] = "Replace product(s)"
|
|
action["context"] = dict(self.env.context)
|
|
action["context"].update(
|
|
active_id=self.id,
|
|
active_ids=self.ids,
|
|
rma_delivery_type="replace",
|
|
)
|
|
return action
|
|
|
|
def action_return(self):
|
|
"""Invoked when 'Return to customer' button in rma form
|
|
view is clicked.
|
|
"""
|
|
self.ensure_one()
|
|
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")
|
|
.with_context(active_id=self.id)
|
|
.read()[0]
|
|
)
|
|
action["context"] = dict(self.env.context)
|
|
action["context"].update(
|
|
active_id=self.id,
|
|
active_ids=self.ids,
|
|
rma_delivery_type="return",
|
|
)
|
|
return action
|
|
|
|
def action_split(self):
|
|
"""Invoked when 'Split' button in rma form view is clicked."""
|
|
self.ensure_one()
|
|
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")
|
|
.with_context(active_id=self.id)
|
|
.read()[0]
|
|
)
|
|
action["context"] = dict(self.env.context)
|
|
action["context"].update(active_id=self.id, active_ids=self.ids)
|
|
return action
|
|
|
|
def action_finish(self):
|
|
"""Invoked when a user wants to manually finalize the RMA"""
|
|
self.ensure_one()
|
|
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")
|
|
.with_context(active_id=self.id)
|
|
.read()[0]
|
|
)
|
|
action["context"] = dict(self.env.context)
|
|
action["context"].update(active_id=self.id, active_ids=self.ids)
|
|
return action
|
|
|
|
def action_cancel(self):
|
|
"""Invoked when 'Cancel' button in rma form view is clicked."""
|
|
self.mapped("reception_move_id")._action_cancel()
|
|
self.write({"state": "cancelled"})
|
|
|
|
def action_draft(self):
|
|
cancelled_rma = self.filtered(lambda r: r.state == "cancelled")
|
|
cancelled_rma.write({"state": "draft"})
|
|
|
|
def action_lock(self):
|
|
"""Invoked when 'Lock' button in rma form view is clicked."""
|
|
self.filtered("can_be_locked").write({"state": "locked"})
|
|
|
|
def action_unlock(self):
|
|
"""Invoked when 'Unlock' button in rma form view is clicked."""
|
|
locked_rma = self.filtered(lambda r: r.state == "locked")
|
|
locked_rma.write({"state": "received"})
|
|
|
|
def action_preview(self):
|
|
"""Invoked when 'Preview' button in rma form view is clicked."""
|
|
self.ensure_one()
|
|
return {
|
|
"type": "ir.actions.act_url",
|
|
"target": "self",
|
|
"url": self.get_portal_url(),
|
|
}
|
|
|
|
def action_view_receipt(self):
|
|
"""Invoked when 'Receipt' smart button in rma form view is clicked."""
|
|
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")
|
|
.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,
|
|
)
|
|
return action
|
|
|
|
def action_view_refund(self):
|
|
"""Invoked when 'Refund' smart button in rma form view is clicked."""
|
|
self.ensure_one()
|
|
return {
|
|
"name": _("Refund"),
|
|
"type": "ir.actions.act_window",
|
|
"view_type": "form",
|
|
"view_mode": "form",
|
|
"res_model": "account.move",
|
|
"views": [(self.env.ref("account.view_move_form").id, "form")],
|
|
"res_id": self.refund_id.id,
|
|
}
|
|
|
|
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")
|
|
.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
|
|
|
|
# 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', 'partner_shipping_id',
|
|
'product_id', 'location_id'
|
|
]
|
|
|
|
This method is intended to be called on confirm RMA action and is
|
|
invoked by:
|
|
rma._check_required_after_draft
|
|
rma.action_confirm
|
|
"""
|
|
ir_translation = self.env["ir.translation"]
|
|
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):
|
|
desc += "\n%s" % ir_translation.get_field_string("rma")[field]
|
|
if desc:
|
|
raise ValidationError(_("Required field(s):%s") % desc)
|
|
|
|
def _ensure_can_be_returned(self):
|
|
"""This method is intended to be invoked after user click on
|
|
'Replace' or 'Return to customer' button (before the delivery wizard
|
|
is launched) and after confirm the wizard.
|
|
|
|
This method is invoked by:
|
|
rma.action_replace
|
|
rma.action_return
|
|
rma.create_replace
|
|
rma.create_return
|
|
"""
|
|
if len(self) == 1:
|
|
if not self.can_be_returned:
|
|
raise ValidationError(_("This RMA cannot perform a return."))
|
|
elif not self.filtered("can_be_returned"):
|
|
raise ValidationError(_("None of the selected RMAs can perform a return."))
|
|
|
|
def _ensure_can_be_replaced(self):
|
|
"""This method is intended to be invoked after user click on
|
|
'Replace' button (before the delivery wizard
|
|
is launched) and after confirm the wizard.
|
|
|
|
This method is invoked by:
|
|
rma.action_replace
|
|
rma.create_replace
|
|
"""
|
|
if len(self) == 1:
|
|
if not self.can_be_replaced:
|
|
raise ValidationError(_("This RMA cannot perform a replacement."))
|
|
elif not self.filtered("can_be_replaced"):
|
|
raise ValidationError(
|
|
_("None of the selected RMAs can perform a replacement.")
|
|
)
|
|
|
|
def _ensure_can_be_split(self):
|
|
"""intended to be called before launch and after save the split wizard.
|
|
invoked by:
|
|
rma.action_split
|
|
rma.extract_quantity
|
|
"""
|
|
self.ensure_one()
|
|
if not self.can_be_split:
|
|
raise ValidationError(_("This RMA cannot be split."))
|
|
|
|
def _ensure_qty_to_return(self, qty=None, uom=None):
|
|
"""This method is intended to be invoked after confirm the wizard.
|
|
invoked by: rma.create_return
|
|
"""
|
|
if qty and uom:
|
|
if uom != self.product_uom:
|
|
qty = uom._compute_quantity(qty, self.product_uom)
|
|
if qty > self.remaining_qty:
|
|
raise ValidationError(
|
|
_("The quantity to return is greater than " "remaining quantity.")
|
|
)
|
|
|
|
def _ensure_qty_to_extract(self, qty, uom):
|
|
"""This method is intended to be invoked after confirm the wizard.
|
|
invoked by: rma.extract_quantity
|
|
"""
|
|
to_split_uom_qty = qty
|
|
if uom != self.product_uom:
|
|
to_split_uom_qty = uom._compute_quantity(qty, self.product_uom)
|
|
if to_split_uom_qty > self.remaining_qty:
|
|
raise ValidationError(
|
|
_(
|
|
"Quantity to extract cannot be greater than remaining "
|
|
"delivery quantity (%s %s)"
|
|
)
|
|
% (self.remaining_qty, self.product_uom.name)
|
|
)
|
|
|
|
# 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()
|
|
self._ensure_can_be_split()
|
|
self._ensure_qty_to_extract(qty, uom)
|
|
self.product_uom_qty -= uom._compute_quantity(qty, self.product_uom)
|
|
if self.remaining_qty_to_done <= 0:
|
|
if self.state == "waiting_return":
|
|
self.state = "returned"
|
|
elif self.state == "waiting_replacement":
|
|
self.state = "replaced"
|
|
extracted_rma = self.copy(
|
|
{
|
|
"origin": self.name,
|
|
"product_uom_qty": qty,
|
|
"product_uom": uom.id,
|
|
"state": "received",
|
|
"reception_move_id": self.reception_move_id.id,
|
|
"origin_split_rma_id": self.id,
|
|
}
|
|
)
|
|
extracted_rma.message_post_with_view(
|
|
"mail.message_origin_link",
|
|
values={"self": extracted_rma, "origin": self},
|
|
subtype_id=self.env.ref("mail.mt_note").id,
|
|
)
|
|
self.message_post(
|
|
body=_(
|
|
'Split: <a href="#" data-oe-model="rma" '
|
|
'data-oe-id="%d">%s</a> has been created.'
|
|
)
|
|
% (
|
|
extracted_rma.id,
|
|
extracted_rma.name,
|
|
)
|
|
)
|
|
return extracted_rma
|
|
|
|
# Refund business methods
|
|
def _prepare_refund(self, invoice_form, origin):
|
|
"""Hook method for preparing the refund Form.
|
|
|
|
This method could be override in order to add new custom field
|
|
values in the refund creation.
|
|
|
|
invoked by:
|
|
rma.action_refund
|
|
"""
|
|
self.ensure_one()
|
|
invoice_form.partner_id = self.partner_invoice_id
|
|
|
|
def _prepare_refund_line(self, line_form):
|
|
"""Hook method for preparing a refund line Form.
|
|
|
|
This method could be override in order to add new custom field
|
|
values in the refund line creation.
|
|
|
|
invoked by:
|
|
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 {}
|
|
|
|
# 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)
|
|
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,
|
|
)
|
|
group_dict.setdefault(key, self.env["rma"])
|
|
group_dict[key] |= record
|
|
for rmas in group_dict.values():
|
|
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()
|
|
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: <a href="#" data-oe-model="stock.picking" '
|
|
'data-oe-id="%d">%s</a> has been created.'
|
|
)
|
|
% (picking.id, picking.name)
|
|
)
|
|
picking.action_confirm()
|
|
picking.action_assign()
|
|
picking.message_post_with_view(
|
|
"mail.message_origin_link",
|
|
values={"self": picking, "origin": rmas},
|
|
subtype_id=self.env.ref("mail.mt_note").id,
|
|
)
|
|
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_returning_move(
|
|
self, move_form, scheduled_date, quantity=None, uom=None
|
|
):
|
|
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
|
|
|
|
# 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)
|
|
new_move = self.delivery_move_ids - moves_before
|
|
if new_move:
|
|
self.reception_move_id.move_dest_ids = [(4, new_move.id)]
|
|
self.message_post(
|
|
body=_(
|
|
"Replacement: "
|
|
'Move <a href="#" data-oe-model="stock.move" '
|
|
'data-oe-id="%d">%s</a> (Picking <a href="#" '
|
|
'data-oe-model="stock.picking" data-oe-id="%d">%s</a>) '
|
|
"has been created."
|
|
)
|
|
% (
|
|
new_move.id,
|
|
new_move.name_get()[0][1],
|
|
new_move.picking_id.id,
|
|
new_move.picking_id.name,
|
|
)
|
|
)
|
|
else:
|
|
self.message_post(
|
|
body=_(
|
|
"Replacement:<br/>"
|
|
'Product <a href="#" data-oe-model="product.product" '
|
|
'data-oe-id="%d">%s</a><br/>'
|
|
"Quantity %f %s<br/>"
|
|
"This replacement did not create a new move, but one of "
|
|
"the previously created moves was updated with this data."
|
|
)
|
|
% (product.id, product.display_name, qty, 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):
|
|
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.
|
|
"""
|
|
if custom_values is None:
|
|
custom_values = {}
|
|
subject = msg_dict.get("subject", "")
|
|
body = html2plaintext(msg_dict.get("body", ""))
|
|
desc = _("<b>E-mail subject:</b> %s<br/><br/><b>E-mail body:</b><br/>%s") % (
|
|
subject,
|
|
body,
|
|
)
|
|
defaults = {
|
|
"description": desc,
|
|
"name": _("New"),
|
|
"origin": _("Incoming e-mail"),
|
|
}
|
|
if msg_dict.get("author_id"):
|
|
partner = self.env["res.partner"].browse(msg_dict.get("author_id"))
|
|
defaults.update(
|
|
partner_id=partner.id,
|
|
partner_invoice_id=partner.address_get(["invoice"]).get(
|
|
"invoice", False
|
|
),
|
|
)
|
|
if msg_dict.get("priority"):
|
|
defaults["priority"] = msg_dict.get("priority")
|
|
defaults.update(custom_values)
|
|
rma = super().message_new(msg_dict, custom_values=defaults)
|
|
if rma.user_id and rma.user_id.partner_id not in rma.message_partner_ids:
|
|
rma.message_subscribe([rma.user_id.partner_id.id])
|
|
return rma
|
|
|
|
@api.returns("mail.message", lambda value: value.id)
|
|
def message_post(self, **kwargs):
|
|
"""Set 'sent' field to True when an email is sent from rma form
|
|
view. This field (sent) is used to set the appropriate style to the
|
|
'Send by Email' button in the rma form view.
|
|
"""
|
|
if self.env.context.get("mark_rma_as_sent"):
|
|
self.write({"sent": True})
|
|
# mail_post_autofollow=True to include email recipient contacts as
|
|
# RMA followers
|
|
self_with_context = self.with_context(mail_post_autofollow=True)
|
|
return super(Rma, self_with_context).message_post(**kwargs)
|
|
|
|
def _message_get_suggested_recipients(self):
|
|
recipients = super()._message_get_suggested_recipients()
|
|
try:
|
|
for record in self.filtered("partner_id"):
|
|
record._message_add_suggested_recipient(
|
|
recipients, partner=record.partner_id, reason=_("Customer")
|
|
)
|
|
except AccessError: # no read access rights
|
|
pass
|
|
return recipients
|
|
|
|
# Reporting business methods
|
|
def _get_report_base_filename(self):
|
|
self.ensure_one()
|
|
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
|
|
[stock.move]._action_cancel
|
|
"""
|
|
rma = self.filtered(lambda r: r.delivered_qty == 0)
|
|
if rma:
|
|
rma.write({"state": "received"})
|
|
|
|
def update_replaced_state(self):
|
|
"""Invoked by:
|
|
[stock.move]._action_done
|
|
[stock.move].unlink
|
|
[stock.move]._action_cancel
|
|
"""
|
|
rma = self.filtered(
|
|
lambda r: (
|
|
r.state == "waiting_replacement"
|
|
and 0 >= r.remaining_qty_to_done == r.remaining_qty
|
|
)
|
|
)
|
|
if rma:
|
|
rma.write({"state": "replaced"})
|
|
|
|
def update_returned_state(self):
|
|
""" Invoked by [stock.move]._action_done"""
|
|
rma = self.filtered(
|
|
lambda r: (r.state == "waiting_return" and r.remaining_qty_to_done <= 0)
|
|
)
|
|
if rma:
|
|
rma.write({"state": "returned"})
|