Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import sale_order_block_wizard

View File

@@ -0,0 +1,312 @@
from markupsafe import Markup
from odoo import _, api, exceptions, fields, models
from odoo.tools import groupby
class SaleOrderBlockWizard(models.TransientModel):
_name = "sale.order.block.wizard"
_description = "Sale Order Block Wizard"
_transient_max_hours = 0.25 # 15 minutes until destroyed
sale_line_block_ids = fields.One2many(
comodel_name="sale.order.block.wizard.line",
inverse_name="wizard_id",
string="Sale Block Lines",
)
confirmation_allowed = fields.Boolean(
string="Allowed to confirm",
compute="_compute_confirmation_allowed",
)
is_uom_adjustable = fields.Boolean(
compute="_compute_is_adjustable",
store=True,
readonly=True,
compute_sudo=True,
)
is_packaging_adjustable = fields.Boolean(
compute="_compute_is_adjustable",
store=True,
readonly=True,
compute_sudo=True,
)
@api.depends_context("uid")
@api.depends("sale_line_block_ids.company_id")
def _compute_confirmation_allowed(self):
"""Compute if the user is allowed to confirm the sale orders."""
self.confirmation_allowed = (
self.env.user
in self.sale_line_block_ids.company_id.sale_line_block_allowed_groups.users
)
@api.depends(
"sale_line_block_ids.product_packaging_allowed_max_qty",
"sale_line_block_ids.product_uom_allowed_max_qty",
)
def _compute_is_adjustable(self):
"""Compute if the sale lines are adjustable."""
for record in self:
lines = record.mapped("sale_line_block_ids")
record.is_packaging_adjustable = bool(
lines.filtered(
lambda line: line.product_packaging_allowed_max_qty > 0.0
)
)
record.is_uom_adjustable = bool(
lines.filtered(lambda line: line.product_uom_allowed_max_qty > 0.0)
)
def confirm(self):
"""Confirm the sale orders ignoring next possible wizards."""
if not all(self.mapped("confirmation_allowed")):
raise exceptions.UserError(
_("You are not allowed to confirm these orders.")
)
orders = self.mapped("sale_line_block_ids.order_id")
orders.message_post(
body=_("Order confirmed with errors by %s.", self.env.user.name),
subtype_id=self.env.ref("mail.mt_note").id,
)
return orders.with_context(skip_block_no_stock_check=True).action_confirm()
def action_adjust_uom_quantity(self):
"""Adjust the quantity of the sale lines to the maximum allowed by the
UoM."""
return self.sale_line_block_ids._action_adjust_uom_quantity()
def action_adjust_packaging_quantity(self):
"""Adjust the quantity of the sale lines to the maximum allowed by the
packaging."""
return self.sale_line_block_ids._action_adjust_packaging_quantity()
def action_move_to_new_order(self):
"""Move the sale lines to a new sale order."""
return self.sale_line_block_ids._action_move_to_new_order()
@api.constrains("sale_line_block_ids")
def _check_sale_line_block_ids(self):
"""Check that all sale lines are from the same company."""
for record in self:
companies = record.mapped(
"sale_line_block_ids.sale_line_id.order_id.company_id"
)
if len(companies) > 1:
raise exceptions.UserError(
_(
"""Cannot launch wizard from sale orders
from different companies."""
)
)
class SaleOrderBlockWizardLine(models.TransientModel):
_name = "sale.order.block.wizard.line"
_description = "Sale Order Block Wizard Line"
_transient_max_hours = 0.25 # 15 minutes until destroyed
wizard_id = fields.Many2one(
comodel_name="sale.order.block.wizard",
string="Wizard",
required=True,
)
sale_line_id = fields.Many2one(
comodel_name="sale.order.line",
string="Sale Line",
required=True,
)
company_id = fields.Many2one(
related="sale_line_id.order_id.company_id",
string="Company",
readonly=True,
store=True,
)
order_id = fields.Many2one(
related="sale_line_id.order_id",
string="Sale Order",
readonly=True,
store=True,
)
product_id = fields.Many2one(
related="sale_line_id.product_id",
string="Product",
readonly=True,
store=True,
)
product_uom_qty = fields.Float(
related="sale_line_id.product_uom_qty",
string="Qty. (UoM)",
readonly=True,
)
product_uom = fields.Many2one(
related="sale_line_id.product_uom",
string="UoM",
readonly=True,
store=True,
)
product_uom_allowed_max_qty = fields.Float(
string="Max. Qty. (UoM)",
compute="_compute_allowed_max_qty",
readonly=True,
store=True,
compute_sudo=True,
)
product_packaging_qty = fields.Float(
related="sale_line_id.product_packaging_qty",
string="Qty. (Pkg.)",
readonly=True,
store=True,
)
product_packaging_id = fields.Many2one(
related="sale_line_id.product_packaging_id",
string="Packaging",
readonly=True,
store=True,
)
product_packaging_allowed_max_qty = fields.Float(
string="Max. Qty. (Pkg.)",
compute="_compute_allowed_max_qty",
readonly=True,
store=True,
compute_sudo=True,
)
@api.depends("sale_line_id", "product_uom_qty", "company_id.sale_line_field_block")
def _compute_allowed_max_qty(self):
"""Compute the maximum allowed quantity by UoM and Packaging of
storable products."""
self.product_uom_allowed_max_qty = 0.0
self.product_packaging_allowed_max_qty = 0.0
for record in self:
field_to_check = record.company_id.sale_line_field_block
if not field_to_check:
self.env.cr.postcommit.add(record.unlink)
continue
if (
record.sale_line_id.product_type != "consu"
and record.sale_line_id.product_id.is_storable
):
self.env.cr.postcommit.add(record.unlink)
continue
allowed_max_qty = record.sale_line_id[field_to_check.name]
if (
allowed_max_qty > 0
and record.sale_line_id.product_uom_qty <= allowed_max_qty
):
self.env.cr.postcommit.add(record.unlink)
continue
record.product_uom_allowed_max_qty = allowed_max_qty
if record.product_packaging_id:
record.product_packaging_allowed_max_qty = (
allowed_max_qty // record.product_packaging_id.qty
)
def _get_adjustable_records(self, packaging=False):
"""Return the records that can be adjusted by UoM or Packaging."""
if packaging:
return self.filtered(
lambda r: r.product_packaging_allowed_max_qty > 0.0
and r.product_packaging_qty > r.product_packaging_allowed_max_qty
)
return self.filtered(
lambda r: r.product_uom_allowed_max_qty > 0.0
and r.product_uom_qty > r.product_uom_allowed_max_qty
)
def _get_reopen_action(self):
"""Return the action to reopen the wizard."""
action = self.env["ir.actions.actions"]._for_xml_id(
"sale_block_no_stock.sale_order_block_wizard_action"
)
action["context"] = {
"default_sale_line_block_ids": [
(0, 0, {"sale_line_id": line.id})
for line in self.mapped("sale_line_id")
]
}
return action
def _action_move_to_new_order(self):
"""Move the sale lines to a new sale order."""
mt_note_id = self.env.ref("mail.mt_note").id
partner_id = self.env.user.partner_id.id
new_orders = self.env["sale.order"].browse()
for order, records in groupby(self, lambda r: r.order_id):
new_order = order.copy(default={"order_line": None})
new_order.message_post_with_source(
"mail.message_origin_link",
render_values={"self": new_order, "origin": order, "edit": True},
subtype_id=mt_note_id,
author_id=partner_id,
)
order.message_post_with_source(
"mail_message_destiny_link_template.message_destiny_link",
render_values={"self": order, "destiny": new_order, "edit": False},
subtype_id=mt_note_id,
author_id=partner_id,
)
for record in records:
record.sale_line_id.write({"order_id": new_order.id})
new_orders |= new_order
return new_orders
@api.model
def _get_adjusted_message(self, product, init_qty, final_qty, uom):
message = Markup(
"""
Product <b>%(product)s</b> adjusted from
<b>%(init_qty)s</b> %(uom)s to <b>%(final_qty)s</b> %(uom)s.
"""
) % {
"product": product,
"init_qty": init_qty,
"final_qty": final_qty,
"uom": uom,
}
return message
def _action_adjust_uom_quantity(self):
"""Adjust the quantity of the sale lines to the maximum allowed by the
UoM."""
mt_note_id = self.env.ref("mail.mt_note").id
adjustable_records = self._get_adjustable_records()
for record in adjustable_records:
record.sale_line_id.order_id.message_post(
body=self._get_adjusted_message(
product=record.product_id.display_name,
init_qty=record.product_uom_qty,
final_qty=record.product_uom_allowed_max_qty,
uom=record.product_uom.name,
),
subtype_id=mt_note_id,
)
record.sale_line_id.product_uom_qty = record.product_uom_allowed_max_qty
if not self - adjustable_records:
return
return (self - adjustable_records)._get_reopen_action()
def _action_adjust_packaging_quantity(self):
"""Adjust the quantity of the sale lines to the maximum allowed by the
packaging."""
mt_note_id = self.env.ref("mail.mt_note").id
adjustable_records = self._get_adjustable_records(packaging=True)
for record in adjustable_records:
record.sale_line_id.order_id.message_post(
body=self._get_adjusted_message(
product=record.product_id.display_name,
init_qty=record.product_packaging_qty,
final_qty=record.product_packaging_allowed_max_qty,
uom=record.product_packaging_id.display_name,
),
subtype_id=mt_note_id,
)
record.sale_line_id.product_packaging_qty = (
record.product_packaging_allowed_max_qty
)
record.sale_line_id.product_uom_qty = (
record.product_packaging_id.qty
* record.product_packaging_allowed_max_qty
)
if not self - adjustable_records:
return
return (self - adjustable_records)._get_reopen_action()

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="sale_order_block_wizard_view" model="ir.ui.view">
<field name="name">Sale Order Block</field>
<field name="model">sale.order.block.wizard</field>
<field name="arch" type="xml">
<form string="Sale Order Block">
<field name="confirmation_allowed" invisible="1" />
<field name="is_uom_adjustable" invisible="1" />
<field name="is_packaging_adjustable" invisible="1" />
<p class="text-muted">
This order cannot be confirmed because it contains products that are not in stock. Please manage the following products before confirming the order.
</p>
<group
name="blocked_lines_group"
staction_create_sale_orderring="Blocked lines"
>
<field name="sale_line_block_ids" nolabel="1" colspan="2">
<list
decoration-danger="product_uom_allowed_max_qty &lt;= 0.0"
create="false"
edit="false"
no_open="true"
>
<field name="wizard_id" column_invisible="1" />
<field name="sale_line_id" column_invisible="1" />
<field name="company_id" column_invisible="1" />
<field name="order_id" optional="hide" />
<field name="product_id" />
<field name="product_uom_qty" optional="show" />
<field name="product_uom_allowed_max_qty" optional="show" />
<field name="product_uom" optional="show" />
<field
name="product_packaging_qty"
optional="show"
invisible="not product_packaging_id"
groups="product.group_stock_packaging"
/>
<field
name="product_packaging_allowed_max_qty"
optional="show"
invisible="not product_packaging_id"
groups="product.group_stock_packaging"
/>
<field
name="product_packaging_id"
optional="show"
invisible="not product_packaging_id"
groups="product.group_stock_packaging"
/>
</list>
</field>
</group>
<footer>
<button
name="action_adjust_uom_quantity"
string="Adjust UoM Quantity"
type="object"
class="btn-primary"
invisible="not is_uom_adjustable or is_packaging_adjustable or not sale_line_block_ids"
/>
<button
name="action_adjust_packaging_quantity"
string="Adjust Packaging Quantity"
type="object"
class="btn-primary"
invisible="not(is_packaging_adjustable and sale_line_block_ids)"
groups="product.group_stock_packaging"
/>
<button
name="action_move_to_new_order"
string="Move to New Order"
type="object"
class="btn-primary"
invisible="is_uom_adjustable or not sale_line_block_ids"
confirm="You are going to move pending lines to a new order"
/>
<button
name="action_move_to_new_order"
string="Move to New Order"
type="object"
class="btn-secondary"
invisible="not(is_uom_adjustable and sale_line_block_ids)"
confirm="You are going to move pending lines to a new order"
/>
<button
name="confirm"
string="Confirm anyway"
type="object"
class="btn-secondary"
invisible="not(confirmation_allowed and sale_line_block_ids)"
confirm="You are going to confirm the Sale Order with not solved lines"
/>
<p class="m-2">or</p>
<button string="Close" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="sale_order_block_wizard_action" model="ir.actions.act_window">
<field name="name">Sale Order Blocked to Confirm</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order.block.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>