Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -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

View file

@ -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()

View file

@ -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",
)

View file

@ -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,
)

View 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

View 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