mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-24 17:12:02 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
|
|
@ -0,0 +1,6 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from . import account_move
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import rma
|
||||
from . import sale
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2023 Tecnativa - Pedro M. Baeza
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def button_cancel(self):
|
||||
"""If this a refund linked to an RMA, undo the linking of the reception move for
|
||||
having proper quantities and status.
|
||||
"""
|
||||
for rma in self.env["rma"].sudo().search([("refund_id", "in", self.ids)]):
|
||||
if rma.sale_line_id:
|
||||
rma._unlink_refund_with_reception_move()
|
||||
return super().button_cancel()
|
||||
|
||||
def button_draft(self):
|
||||
"""Relink the reception move when passing the refund again to draft."""
|
||||
for rma in self.env["rma"].sudo().search([("refund_id", "in", self.ids)]):
|
||||
if rma.sale_line_id:
|
||||
rma._link_refund_with_reception_move()
|
||||
return super().button_draft()
|
||||
|
||||
def unlink(self):
|
||||
"""If the invoice is removed, rollback the quantities correction"""
|
||||
for rma in self.invoice_line_ids.rma_id.filtered("sale_line_id"):
|
||||
rma._unlink_refund_with_reception_move()
|
||||
return super().unlink()
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# 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,
|
||||
)
|
||||
221
odoo-bringout-oca-rma-rma_sale/rma_sale/models/rma.py
Normal file
221
odoo-bringout-oca-rma-rma_sale/rma_sale/models/rma.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Tecnativa - Pedro M. Baeza
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class Rma(models.Model):
|
||||
_inherit = "rma"
|
||||
|
||||
order_id = fields.Many2one(
|
||||
comodel_name="sale.order",
|
||||
string="Sale Order",
|
||||
domain="["
|
||||
" ('partner_id', 'child_of', commercial_partner_id),"
|
||||
" ('state', 'in', ['sale', 'done']),"
|
||||
"]",
|
||||
store=True,
|
||||
readonly=False,
|
||||
compute="_compute_order_id",
|
||||
)
|
||||
allowed_picking_ids = fields.Many2many(
|
||||
comodel_name="stock.picking",
|
||||
compute="_compute_allowed_picking_ids",
|
||||
)
|
||||
picking_id = fields.Many2one(
|
||||
domain="(order_id or partner_id) and [('id', 'in', allowed_picking_ids)] or "
|
||||
"[('state', '=', 'done'), ('picking_type_id.code', '=', 'outgoing')] "
|
||||
)
|
||||
allowed_move_ids = fields.Many2many(
|
||||
comodel_name="stock.move",
|
||||
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",
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
domain="order_id and [('id', 'in', allowed_product_ids)] or "
|
||||
"[('type', 'in', ['consu', 'product'])]"
|
||||
)
|
||||
# Add index to this field, as we perform a search on it
|
||||
refund_id = fields.Many2one(index=True)
|
||||
|
||||
@api.depends("partner_id", "order_id")
|
||||
def _compute_allowed_picking_ids(self):
|
||||
domain = [("state", "=", "done"), ("picking_type_id.code", "=", "outgoing")]
|
||||
for rec in self:
|
||||
domain2 = domain.copy()
|
||||
if rec.partner_id:
|
||||
commercial_partner = rec.partner_id.commercial_partner_id
|
||||
domain2.append(("partner_id", "child_of", commercial_partner.id))
|
||||
if rec.order_id:
|
||||
domain2.append(("sale_id", "=", rec.order_id.id))
|
||||
if domain2 != domain:
|
||||
rec.allowed_picking_ids = self.env["stock.picking"].search(domain2)
|
||||
else:
|
||||
rec.allowed_picking_ids = False # don't populate a big list
|
||||
|
||||
@api.depends("order_id", "picking_id")
|
||||
def _compute_allowed_move_ids(self):
|
||||
for rec in self:
|
||||
if rec.order_id:
|
||||
order_move = rec.order_id.order_line.mapped("move_ids")
|
||||
rec.allowed_move_ids = order_move.filtered(
|
||||
lambda r: r.picking_id == self.picking_id and r.state == "done"
|
||||
).ids
|
||||
else:
|
||||
rec.allowed_move_ids = self.picking_id.move_ids.ids
|
||||
|
||||
@api.depends("order_id")
|
||||
def _compute_allowed_product_ids(self):
|
||||
for rec in self:
|
||||
if rec.order_id:
|
||||
order_product = rec.order_id.order_line.mapped("product_id")
|
||||
rec.allowed_product_ids = order_product.filtered(
|
||||
lambda r: r.type in ["consu", "product"]
|
||||
).ids
|
||||
else:
|
||||
rec.allowed_product_ids = False # don't populate a big list
|
||||
|
||||
@api.depends("partner_id")
|
||||
def _compute_order_id(self):
|
||||
"""Empty sales order when changing partner."""
|
||||
self.order_id = False
|
||||
|
||||
@api.onchange("order_id")
|
||||
def _onchange_order_id(self):
|
||||
self.product_id = self.picking_id = False
|
||||
|
||||
def _link_refund_with_reception_move(self):
|
||||
"""Perform the internal operations for linking the RMA reception move with the
|
||||
sales order line if applicable.
|
||||
"""
|
||||
self.ensure_one()
|
||||
move = self.reception_move_id
|
||||
if (
|
||||
move
|
||||
and float_compare(
|
||||
self.product_uom_qty,
|
||||
move.product_uom_qty,
|
||||
precision_rounding=move.product_uom.rounding,
|
||||
)
|
||||
== 0
|
||||
):
|
||||
self.reception_move_id.sale_line_id = self.sale_line_id.id
|
||||
self.reception_move_id.to_refund = True
|
||||
|
||||
def _unlink_refund_with_reception_move(self):
|
||||
"""Perform the internal operations for unlinking the RMA reception move with the
|
||||
sales order line.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.reception_move_id.sale_line_id = False
|
||||
self.reception_move_id.to_refund = False
|
||||
|
||||
def action_refund(self):
|
||||
"""As we have made a refund, the return move + the refund should be linked to
|
||||
the source sales order line, to decrease both the delivered and invoiced
|
||||
quantity.
|
||||
|
||||
NOTE: The refund line is linked to the SO line in `_prepare_refund_line`.
|
||||
"""
|
||||
res = super().action_refund()
|
||||
for rma in self:
|
||||
if rma.sale_line_id:
|
||||
rma._link_refund_with_reception_move()
|
||||
return res
|
||||
|
||||
def _prepare_refund_vals(self, origin=False):
|
||||
"""Inject salesman from sales order (if any)"""
|
||||
vals = super()._prepare_refund_vals(origin=origin)
|
||||
if self.order_id:
|
||||
vals["invoice_user_id"] = self.order_id.user_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_refund_line_vals(self):
|
||||
"""Add line data and link to the sales order, only if the RMA is for the whole
|
||||
move quantity. In other cases, incorrect delivered/invoiced quantities will be
|
||||
logged on the sales order, so better to let the operations not linked.
|
||||
"""
|
||||
vals = super()._prepare_refund_line_vals()
|
||||
line = self.sale_line_id
|
||||
if line:
|
||||
vals["product_id"] = line.product_id.id
|
||||
vals["price_unit"] = line.price_unit
|
||||
vals["discount"] = line.discount
|
||||
vals["sequence"] = line.sequence
|
||||
move = self.reception_move_id
|
||||
if (
|
||||
move
|
||||
and float_compare(
|
||||
self.product_uom_qty,
|
||||
move.product_uom_qty,
|
||||
precision_rounding=move.product_uom.rounding,
|
||||
)
|
||||
== 0
|
||||
):
|
||||
vals["sale_line_ids"] = [(4, line.id)]
|
||||
return vals
|
||||
|
||||
def _prepare_procurement_group_vals(self):
|
||||
vals = super()._prepare_procurement_group_vals()
|
||||
if not self.env.context.get("ignore_rma_sale_order") and self.order_id:
|
||||
vals["sale_id"] = self.order_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_delivery_procurements(self, scheduled_date=None, qty=None, uom=None):
|
||||
self = self.with_context(ignore_rma_sale_order=True)
|
||||
return super()._prepare_delivery_procurements(
|
||||
scheduled_date=scheduled_date, qty=qty, uom=uom
|
||||
)
|
||||
|
||||
def _prepare_delivery_procurement_vals(self, scheduled_date=None):
|
||||
vals = super()._prepare_delivery_procurement_vals(scheduled_date=scheduled_date)
|
||||
if (
|
||||
self.move_id
|
||||
and self.move_id.sale_line_id
|
||||
and self.operation_id.action_create_refund == "update_quantity"
|
||||
):
|
||||
vals["sale_line_id"] = self.move_id.sale_line_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_replace_procurement_vals(self, warehouse=None, scheduled_date=None):
|
||||
vals = super()._prepare_replace_procurement_vals(
|
||||
warehouse=warehouse, scheduled_date=scheduled_date
|
||||
)
|
||||
if (
|
||||
self.move_id
|
||||
and self.move_id.sale_line_id
|
||||
and self.operation_id.action_create_refund == "update_quantity"
|
||||
):
|
||||
vals["sale_line_id"] = self.move_id.sale_line_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_reception_procurement_vals(self, group=None):
|
||||
"""This method is used only for reception and a specific RMA IN route."""
|
||||
vals = super()._prepare_reception_procurement_vals(group=group)
|
||||
if (
|
||||
self.move_id
|
||||
and self.move_id.sale_line_id
|
||||
and self.operation_id.action_create_refund == "update_quantity"
|
||||
):
|
||||
vals["sale_line_id"] = self.move_id.sale_line_id.id
|
||||
return vals
|
||||
|
||||
def create_replace(self, scheduled_date, warehouse, product, qty, uom):
|
||||
# When the procurement group has the sale id set it will propagate to the
|
||||
# pickings. This is inconvenient for this operation as when we confirm the
|
||||
# customer delivery a new order line will be created with the replaced option
|
||||
# which will be set for invoicing.
|
||||
moves_before = self.delivery_move_ids
|
||||
res = super().create_replace(scheduled_date, warehouse, product, qty, uom)
|
||||
new_moves = self.delivery_move_ids - moves_before
|
||||
new_moves.picking_id.sale_id = False
|
||||
return res
|
||||
178
odoo-bringout-oca-rma-rma_sale/rma_sale/models/sale.py
Normal file
178
odoo-bringout-oca-rma-rma_sale/rma_sale/models/sale.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
# RMAs that were created from a sale order
|
||||
rma_ids = fields.One2many(
|
||||
comodel_name="rma",
|
||||
inverse_name="order_id",
|
||||
string="RMAs",
|
||||
copy=False,
|
||||
)
|
||||
rma_count = fields.Integer(string="RMA count", compute="_compute_rma_count")
|
||||
|
||||
def _compute_rma_count(self):
|
||||
rma_data = self.env["rma"].read_group(
|
||||
[("order_id", "in", self.ids)], ["order_id"], ["order_id"]
|
||||
)
|
||||
mapped_data = {r["order_id"][0]: r["order_id_count"] for r in rma_data}
|
||||
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"],
|
||||
"allowed_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"]:
|
||||
raise ValidationError(
|
||||
_("You may only create RMAs from a " "confirmed or done sale order.")
|
||||
)
|
||||
wizard_obj = self.env["sale.order.rma.wizard"]
|
||||
line_vals = [
|
||||
(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(
|
||||
{"line_ids": line_vals, "location_id": self.warehouse_id.rma_loc_id.id}
|
||||
)
|
||||
return {
|
||||
"name": _("Create RMA"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": "sale.order.rma.wizard",
|
||||
"res_id": wizard.id,
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def action_view_rma(self):
|
||||
self.ensure_one()
|
||||
action = self.sudo().env.ref("rma.rma_action").read()[0]
|
||||
rma = self.rma_ids
|
||||
if len(rma) == 1:
|
||||
action.update(
|
||||
res_id=rma.id,
|
||||
view_mode="form",
|
||||
views=[],
|
||||
)
|
||||
else:
|
||||
action["domain"] = [("id", "in", rma.ids)]
|
||||
# reset context to show all related rma without default filters
|
||||
action["context"] = {}
|
||||
return action
|
||||
|
||||
def get_delivery_rma_data(self):
|
||||
self.ensure_one()
|
||||
data = []
|
||||
for line in self.order_line:
|
||||
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"""
|
||||
res = 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)
|
||||
return res
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
def get_delivery_move(self):
|
||||
self.ensure_one()
|
||||
return self.move_ids.filtered(
|
||||
lambda r: (
|
||||
self == r.sale_line_id
|
||||
and r.state == "done"
|
||||
and not r.scrapped
|
||||
and r.location_dest_id.usage == "customer"
|
||||
and (
|
||||
not r.origin_returned_move_id
|
||||
or (r.origin_returned_move_id and r.to_refund)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def prepare_sale_rma_data(self):
|
||||
self.ensure_one()
|
||||
# Method helper to filter chained moves
|
||||
|
||||
def _get_chained_moves(_moves, done_moves=None):
|
||||
moves = _moves.browse()
|
||||
done_moves = done_moves or _moves.browse()
|
||||
for move in _moves:
|
||||
if move.location_dest_id.usage == "customer":
|
||||
moves |= move.returned_move_ids
|
||||
else:
|
||||
moves |= move.move_dest_ids
|
||||
done_moves |= _moves
|
||||
moves = moves.filtered(
|
||||
lambda r: r.state in ["partially_available", "assigned", "done"]
|
||||
)
|
||||
if not moves:
|
||||
return moves
|
||||
moves -= done_moves
|
||||
moves |= _get_chained_moves(moves, done_moves)
|
||||
return moves
|
||||
|
||||
product = self.product_id
|
||||
if self.product_id.type not in ["product", "consu"]:
|
||||
return {}
|
||||
moves = self.get_delivery_move()
|
||||
data = []
|
||||
if moves:
|
||||
for move in moves:
|
||||
# Look for chained moves to check how many items we can allow
|
||||
# to return. When a product is re-delivered it should be
|
||||
# allowed to open an RMA again on it.
|
||||
qty = move.product_uom_qty
|
||||
for _move in _get_chained_moves(move):
|
||||
factor = 1
|
||||
if _move.location_dest_id.usage != "customer":
|
||||
factor = -1
|
||||
qty += factor * _move.product_uom_qty
|
||||
# If by chance we get a negative qty we should ignore it
|
||||
qty = max(0, qty)
|
||||
data.append(
|
||||
{
|
||||
"product": move.product_id,
|
||||
"quantity": qty,
|
||||
"uom": move.product_uom,
|
||||
"picking": move.picking_id,
|
||||
"sale_line_id": self,
|
||||
}
|
||||
)
|
||||
else:
|
||||
data.append(
|
||||
{
|
||||
"product": product,
|
||||
"quantity": self.qty_delivered,
|
||||
"uom": self.product_uom,
|
||||
"picking": False,
|
||||
"sale_line_id": self,
|
||||
}
|
||||
)
|
||||
return data
|
||||
Loading…
Add table
Add a link
Reference in a new issue