Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
1
sale_block_no_stock/wizard/__init__.py
Executable file
1
sale_block_no_stock/wizard/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import sale_order_block_wizard
|
||||
312
sale_block_no_stock/wizard/sale_order_block_wizard.py
Executable file
312
sale_block_no_stock/wizard/sale_order_block_wizard.py
Executable 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()
|
||||
108
sale_block_no_stock/wizard/sale_order_block_wizard_views.xml
Executable file
108
sale_block_no_stock/wizard/sale_order_block_wizard_views.xml
Executable 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 <= 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>
|
||||
Reference in New Issue
Block a user