Files
stock-logistics-warehouse/stock_cycle_count/models/stock_inventory.py
Jordi Ballester Alomar 68d561eb05 [IMP] stock_cycle_count: Implement constrains in inventory adjustment.
The previous method was too restrictive, and would cause conflicts
in cases where another module would introduce changes to the inventory
adjustment that were not really affecting to the cycle count.

We implement the minimum necessary constrains to ensure consistency between
the cycle count and the related inventory adjusment.
2022-11-04 17:12:43 +01:00

133 lines
4.4 KiB
Python

# Copyright 2017-2022 ForgeFlow S.L.
# (http://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
PERCENT = 100.0
class StockInventory(models.Model):
_inherit = "stock.inventory"
@api.depends("state", "line_ids")
def _compute_inventory_accuracy(self):
for inv in self:
theoretical = sum(inv.line_ids.mapped(lambda x: abs(x.theoretical_qty)))
abs_discrepancy = sum(inv.line_ids.mapped(lambda x: abs(x.discrepancy_qty)))
if theoretical:
inv.inventory_accuracy = max(
PERCENT * (theoretical - abs_discrepancy) / theoretical, 0.0
)
if not inv.line_ids and inv.state == "done":
inv.inventory_accuracy = PERCENT
cycle_count_id = fields.Many2one(
comodel_name="stock.cycle.count",
string="Stock Cycle Count",
ondelete="restrict",
readonly=True,
)
inventory_accuracy = fields.Float(
string="Accuracy",
compute="_compute_inventory_accuracy",
digits=(3, 2),
store=True,
group_operator="avg",
)
def _update_cycle_state(self):
for inv in self:
if inv.cycle_count_id and inv.state == "done":
inv.cycle_count_id.state = "done"
return True
def _domain_cycle_count_candidate(self):
return [
("state", "=", "draft"),
("location_id", "in", self.location_ids.ids),
]
def _link_to_planned_cycle_count(self):
self.ensure_one()
domain = self._domain_cycle_count_candidate()
candidate = self.env["stock.cycle.count"].search(
domain, limit=1, order="date_deadline asc"
)
# Also find inventories that do not exclude subloations but that are
# for a bin location (no childs). This makes the attachment logic more
# flexible and user friendly (no need to remember to tick the
# non-standard `exclude_sublocation` field).
if (
candidate
and not self.product_ids
and (
self.exclude_sublocation
or (len(self.location_ids) == 1 and not self.location_ids[0].child_ids)
)
):
candidate.state = "open"
self.write({"cycle_count_id": candidate.id, "exclude_sublocation": True})
return True
def action_validate(self):
res = super(StockInventory, self).action_validate()
self._update_cycle_state()
return res
def action_force_done(self):
res = super(StockInventory, self).action_force_done()
self._update_cycle_state()
return res
@api.model
def create(self, vals):
res = super().create(vals)
if not res.cycle_count_id:
res._link_to_planned_cycle_count()
return res
def _is_consistent_with_cycle_count(self):
self.ensure_one()
if (
not self.location_ids
or len(self.location_ids) > 1
or self.location_ids != self.cycle_count_id.location_id
):
return False, _(
"The location in the inventory does not match with the cycle count."
)
if self.product_ids:
return False, _(
"The adjustment should be done for all products in the location."
)
if self.company_id != self.cycle_count_id.company_id:
return False, _(
"The company of the adjustment does not match with the "
"company in the cycle count."
)
if not self.exclude_sublocation:
return False, _(
"An adjustment linked to a cycle count should exclude the sublocations."
)
return True, ""
@api.constrains(
"cycle_count_id",
"location_ids",
"product_ids",
"company_id",
"exclude_sublocation",
)
def _check_cycle_count_consistency(self):
for rec in self.filtered(lambda r: r.cycle_count_id):
is_consistent, msg = rec._is_consistent_with_cycle_count()
if not is_consistent:
raise ValidationError(
_(
"The Inventory Adjustment is inconsistent with the Cycle Count:\n%s"
)
% msg
)