[IMP] stock_picking_batch_custom: configuraciones de bloqueo por pestaña
This commit is contained in:
parent
05a8908007
commit
18fd73ed1b
7 changed files with 211 additions and 25 deletions
|
|
@ -14,6 +14,7 @@
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
|
"views/res_config_settings_views.xml",
|
||||||
"views/stock_move_line_views.xml",
|
"views/stock_move_line_views.xml",
|
||||||
"views/stock_picking_batch_views.xml",
|
"views/stock_picking_batch_views.xml",
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from . import res_company # noqa: F401
|
||||||
|
from . import res_config_settings # noqa: F401
|
||||||
from . import stock_move_line # noqa: F401
|
from . import stock_move_line # noqa: F401
|
||||||
from . import stock_backorder_confirmation # noqa: F401
|
from . import stock_backorder_confirmation # noqa: F401
|
||||||
from . import stock_picking # noqa: F401
|
from . import stock_picking # noqa: F401
|
||||||
|
|
|
||||||
37
stock_picking_batch_custom/models/res_company.py
Normal file
37
stock_picking_batch_custom/models/res_company.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = "res.company"
|
||||||
|
|
||||||
|
batch_summary_restriction_enabled = fields.Boolean(
|
||||||
|
string="Enforce Product Summary Restriction",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
batch_summary_restriction_scope = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("processed", "Only processed products"),
|
||||||
|
("all", "All summary products"),
|
||||||
|
],
|
||||||
|
string="Product Summary Restriction Scope",
|
||||||
|
default="processed",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_detailed_restriction_enabled = fields.Boolean(
|
||||||
|
string="Enforce Detailed Operations Restriction",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
batch_detailed_restriction_scope = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("processed", "Only processed lines"),
|
||||||
|
("all", "All detailed lines"),
|
||||||
|
],
|
||||||
|
string="Detailed Operations Restriction Scope",
|
||||||
|
default="processed",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
27
stock_picking_batch_custom/models/res_config_settings.py
Normal file
27
stock_picking_batch_custom/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = "res.config.settings"
|
||||||
|
|
||||||
|
batch_summary_restriction_enabled = fields.Boolean(
|
||||||
|
related="company_id.batch_summary_restriction_enabled",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
batch_summary_restriction_scope = fields.Selection(
|
||||||
|
related="company_id.batch_summary_restriction_scope",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_detailed_restriction_enabled = fields.Boolean(
|
||||||
|
related="company_id.batch_detailed_restriction_enabled",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
batch_detailed_restriction_scope = fields.Selection(
|
||||||
|
related="company_id.batch_detailed_restriction_scope",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|
@ -140,15 +140,47 @@ class StockPickingBatch(models.Model):
|
||||||
else:
|
else:
|
||||||
batch.summary_line_ids = [fields.Command.clear()]
|
batch.summary_line_ids = [fields.Command.clear()]
|
||||||
|
|
||||||
def _check_all_products_collected(self, pickings=None):
|
def _raise_collected_restriction(self, base_message, product_names):
|
||||||
"""Validate only Detailed Operations collected flags.
|
message = self.env._(base_message)
|
||||||
|
if product_names:
|
||||||
|
message += "\n" + self.env._(
|
||||||
|
"Pending products: %(products)s", products=product_names
|
||||||
|
)
|
||||||
|
raise UserError(message)
|
||||||
|
|
||||||
Product Summary checkboxes are informative and must not block the flow.
|
def _get_not_collected_summary_lines(self, batch_pickings, scope):
|
||||||
The blocking validation applies to stock.move.line.is_collected in the
|
summary_lines = self.summary_line_ids
|
||||||
detailed operations tab for lines with processed quantity.
|
if scope == "processed":
|
||||||
"""
|
processed_product_ids = set(
|
||||||
|
batch_pickings.move_line_ids.filtered(
|
||||||
|
lambda line: (
|
||||||
|
line.move_id.state not in ("cancel", "done")
|
||||||
|
and line.product_id
|
||||||
|
and line.move_id.quantity > 0
|
||||||
|
)
|
||||||
|
).mapped("product_id.id")
|
||||||
|
)
|
||||||
|
summary_lines = summary_lines.filtered(
|
||||||
|
lambda line: line.product_id.id in processed_product_ids
|
||||||
|
)
|
||||||
|
return summary_lines.filtered(lambda line: not line.is_collected)
|
||||||
|
|
||||||
|
def _get_not_collected_detailed_lines(self, batch_pickings, scope):
|
||||||
|
detailed_lines = batch_pickings.move_line_ids.filtered(
|
||||||
|
lambda line: line.move_id.state not in ("cancel", "done")
|
||||||
|
and line.product_id
|
||||||
|
)
|
||||||
|
if scope == "processed":
|
||||||
|
detailed_lines = detailed_lines.filtered(
|
||||||
|
lambda line: line.move_id.quantity > 0
|
||||||
|
)
|
||||||
|
return detailed_lines.filtered(lambda line: not line.is_collected)
|
||||||
|
|
||||||
|
def _check_all_products_collected(self, pickings=None):
|
||||||
|
"""Validate collected restrictions based on tab configuration."""
|
||||||
|
|
||||||
for batch in self:
|
for batch in self:
|
||||||
|
company = batch.company_id or self.env.company
|
||||||
batch_pickings = (
|
batch_pickings = (
|
||||||
pickings.filtered(lambda p, batch=batch: p.batch_id == batch)
|
pickings.filtered(lambda p, batch=batch: p.batch_id == batch)
|
||||||
if pickings
|
if pickings
|
||||||
|
|
@ -157,28 +189,37 @@ class StockPickingBatch(models.Model):
|
||||||
if not batch_pickings:
|
if not batch_pickings:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
not_collected_move_lines = batch_pickings.move_line_ids.filtered(
|
if company.batch_detailed_restriction_enabled:
|
||||||
lambda line: (
|
not_collected_detailed = batch._get_not_collected_detailed_lines(
|
||||||
line.move_id.state not in ("cancel", "done")
|
batch_pickings, company.batch_detailed_restriction_scope
|
||||||
and line.product_id
|
|
||||||
and line.move_id.quantity > 0
|
|
||||||
and not line.is_collected
|
|
||||||
)
|
)
|
||||||
)
|
if not_collected_detailed:
|
||||||
if not not_collected_move_lines:
|
|
||||||
continue
|
|
||||||
|
|
||||||
product_names = ", ".join(
|
product_names = ", ".join(
|
||||||
sorted(set(not_collected_move_lines.mapped("product_id.display_name")))
|
sorted(
|
||||||
|
set(
|
||||||
|
not_collected_detailed.mapped("product_id.display_name")
|
||||||
)
|
)
|
||||||
message = batch.env._(
|
|
||||||
"You must mark detailed operation lines as collected before validating the batch."
|
|
||||||
)
|
)
|
||||||
if product_names:
|
|
||||||
message += "\n" + batch.env._(
|
|
||||||
"Pending products: %(products)s", products=product_names
|
|
||||||
)
|
)
|
||||||
raise UserError(message)
|
batch._raise_collected_restriction(
|
||||||
|
"You must mark detailed operation lines as collected before validating the batch.",
|
||||||
|
product_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
if company.batch_summary_restriction_enabled:
|
||||||
|
not_collected_summary = batch._get_not_collected_summary_lines(
|
||||||
|
batch_pickings, company.batch_summary_restriction_scope
|
||||||
|
)
|
||||||
|
if not_collected_summary:
|
||||||
|
product_names = ", ".join(
|
||||||
|
sorted(
|
||||||
|
set(not_collected_summary.mapped("product_id.display_name"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
batch._raise_collected_restriction(
|
||||||
|
"You must mark product summary lines as collected before validating the batch.",
|
||||||
|
product_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StockPickingBatchSummaryLine(models.Model):
|
class StockPickingBatchSummaryLine(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,12 @@ class TestBatchSummary(TransactionCase):
|
||||||
batch._compute_summary_line_ids()
|
batch._compute_summary_line_ids()
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
|
def _set_batch_restriction_config(self, **vals):
|
||||||
|
company = self.env.company
|
||||||
|
previous = {key: company[key] for key in vals}
|
||||||
|
company.write(vals)
|
||||||
|
self.addCleanup(lambda: company.write(previous))
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
def test_totals_and_pending_with_conversion(self):
|
def test_totals_and_pending_with_conversion(self):
|
||||||
"""Totals aggregate per product with UoM conversion and pending."""
|
"""Totals aggregate per product with UoM conversion and pending."""
|
||||||
|
|
@ -421,3 +427,38 @@ class TestBatchSummary(TransactionCase):
|
||||||
|
|
||||||
# Should not raise
|
# Should not raise
|
||||||
batch._check_all_products_collected()
|
batch._check_all_products_collected()
|
||||||
|
|
||||||
|
def test_summary_restriction_blocks_when_enabled(self):
|
||||||
|
"""Product Summary restriction can be enabled independently."""
|
||||||
|
|
||||||
|
self._set_batch_restriction_config(
|
||||||
|
batch_summary_restriction_enabled=True,
|
||||||
|
batch_summary_restriction_scope="processed",
|
||||||
|
batch_detailed_restriction_enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = self._create_batch_with_pickings()
|
||||||
|
batch.action_confirm()
|
||||||
|
batch._compute_summary_line_ids()
|
||||||
|
|
||||||
|
# Detailed lines are collected, but Product Summary stays unchecked
|
||||||
|
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
|
||||||
|
{"is_collected": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(UserError, "product summary"):
|
||||||
|
batch.action_done()
|
||||||
|
|
||||||
|
def test_detailed_scope_all_blocks_unprocessed_lines(self):
|
||||||
|
"""Detailed scope 'all' must include non-processed detailed lines."""
|
||||||
|
|
||||||
|
self._set_batch_restriction_config(
|
||||||
|
batch_summary_restriction_enabled=False,
|
||||||
|
batch_detailed_restriction_enabled=True,
|
||||||
|
batch_detailed_restriction_scope="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = self._create_partial_batch(extra_move=True)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(UserError, self.product_2.display_name):
|
||||||
|
batch._check_all_products_collected(batch.picking_ids)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form_inherit_batch_custom" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.stock.picking.batch.custom</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="stock.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//app[@name='stock']/block[@name='operations_setting_container']" position="inside">
|
||||||
|
<setting id="batch_product_summary_restrictions" string="Batch Product Summary Restriction">
|
||||||
|
<field name="batch_summary_restriction_enabled"/>
|
||||||
|
<div class="content-group" invisible="not batch_summary_restriction_enabled">
|
||||||
|
<div class="mt8">
|
||||||
|
<label for="batch_summary_restriction_scope"/>
|
||||||
|
<field name="batch_summary_restriction_scope"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Configuración de restricciones para la pestaña Product Summary.
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
|
||||||
|
<setting id="batch_detailed_operations_restrictions" string="Batch Detailed Operations Restriction">
|
||||||
|
<field name="batch_detailed_restriction_enabled"/>
|
||||||
|
<div class="content-group" invisible="not batch_detailed_restriction_enabled">
|
||||||
|
<div class="mt8">
|
||||||
|
<label for="batch_detailed_restriction_scope"/>
|
||||||
|
<field name="batch_detailed_restriction_scope"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
Configuración de restricciones para la pestaña Operaciones Detalladas.
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue