mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[IMP] stock_cycle_count: black, isort
This commit is contained in:
committed by
Mateu Griful
parent
73c7ed377a
commit
1605a9fea4
@@ -4,12 +4,11 @@
|
||||
{
|
||||
"name": "Stock Cycle Count",
|
||||
"summary": "Adds the capability to schedule cycle counts in a "
|
||||
"warehouse through different rules defined by the user.",
|
||||
"warehouse through different rules defined by the user.",
|
||||
"version": "12.0.1.0.1",
|
||||
"development_status": "Mature",
|
||||
"maintainers": ["lreficent"],
|
||||
"author": "Eficent, "
|
||||
"Odoo Community Association (OCA)",
|
||||
"author": "Eficent, " "Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"category": "Warehouse Management",
|
||||
"depends": [
|
||||
|
||||
@@ -2,66 +2,77 @@
|
||||
# (http://www.eficent.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockCycleCount(models.Model):
|
||||
_name = 'stock.cycle.count'
|
||||
_name = "stock.cycle.count"
|
||||
_description = "Stock Cycle Counts"
|
||||
_inherit = 'mail.thread'
|
||||
_inherit = "mail.thread"
|
||||
_order = "id desc"
|
||||
|
||||
@api.model
|
||||
def _default_company(self):
|
||||
company_id = self.env['res.company']._company_default_get(self._name)
|
||||
company_id = self.env["res.company"]._company_default_get(self._name)
|
||||
return company_id
|
||||
|
||||
name = fields.Char(string='Name', readonly=True)
|
||||
name = fields.Char(string="Name", readonly=True)
|
||||
location_id = fields.Many2one(
|
||||
comodel_name='stock.location', string='Location',
|
||||
comodel_name="stock.location",
|
||||
string="Location",
|
||||
required=True,
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
readonly=True,
|
||||
states={"draft": [("readonly", False)]},
|
||||
)
|
||||
responsible_id = fields.Many2one(
|
||||
comodel_name='res.users', string='Assigned to',
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
track_visibility='onchange',
|
||||
comodel_name="res.users",
|
||||
string="Assigned to",
|
||||
readonly=True,
|
||||
states={"draft": [("readonly", False)]},
|
||||
track_visibility="onchange",
|
||||
)
|
||||
date_deadline = fields.Date(
|
||||
string='Required Date',
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
track_visibility='onchange',
|
||||
string="Required Date",
|
||||
readonly=True,
|
||||
states={"draft": [("readonly", False)]},
|
||||
track_visibility="onchange",
|
||||
)
|
||||
cycle_count_rule_id = fields.Many2one(
|
||||
comodel_name='stock.cycle.count.rule', string='Cycle count rule',
|
||||
comodel_name="stock.cycle.count.rule",
|
||||
string="Cycle count rule",
|
||||
required=True,
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
track_visibility='onchange',
|
||||
readonly=True,
|
||||
states={"draft": [("readonly", False)]},
|
||||
track_visibility="onchange",
|
||||
)
|
||||
state = fields.Selection(selection=[
|
||||
('draft', 'Planned'),
|
||||
('open', 'Execution'),
|
||||
('cancelled', 'Cancelled'),
|
||||
('done', 'Done')
|
||||
], string='State', default='draft', track_visibility='onchange',
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("draft", "Planned"),
|
||||
("open", "Execution"),
|
||||
("cancelled", "Cancelled"),
|
||||
("done", "Done"),
|
||||
],
|
||||
string="State",
|
||||
default="draft",
|
||||
track_visibility="onchange",
|
||||
)
|
||||
stock_adjustment_ids = fields.One2many(
|
||||
comodel_name='stock.inventory', inverse_name='cycle_count_id',
|
||||
string='Inventory Adjustment',
|
||||
track_visibility='onchange',
|
||||
)
|
||||
inventory_adj_count = fields.Integer(
|
||||
compute='_compute_inventory_adj_count',
|
||||
comodel_name="stock.inventory",
|
||||
inverse_name="cycle_count_id",
|
||||
string="Inventory Adjustment",
|
||||
track_visibility="onchange",
|
||||
)
|
||||
inventory_adj_count = fields.Integer(compute="_compute_inventory_adj_count")
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company', string='Company',
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
required=True,
|
||||
default=_default_company,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends('stock_adjustment_ids')
|
||||
@api.depends("stock_adjustment_ids")
|
||||
@api.multi
|
||||
def _compute_inventory_adj_count(self):
|
||||
for rec in self:
|
||||
@@ -69,46 +80,43 @@ class StockCycleCount(models.Model):
|
||||
|
||||
@api.multi
|
||||
def do_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
self.write({"state": "cancelled"})
|
||||
|
||||
@api.multi
|
||||
def _prepare_inventory_adjustment(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'INV/{}'.format(self.name),
|
||||
'cycle_count_id': self.id,
|
||||
'location_id': self.location_id.id,
|
||||
'exclude_sublocation': True
|
||||
"name": "INV/{}".format(self.name),
|
||||
"cycle_count_id": self.id,
|
||||
"location_id": self.location_id.id,
|
||||
"exclude_sublocation": True,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code(
|
||||
'stock.cycle.count') or ''
|
||||
vals["name"] = self.env["ir.sequence"].next_by_code("stock.cycle.count") or ""
|
||||
return super(StockCycleCount, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def action_create_inventory_adjustment(self):
|
||||
if any([s != 'draft' for s in self.mapped('state')]):
|
||||
raise UserError(_(
|
||||
"You can only confirm cycle counts in state 'Planned'."
|
||||
))
|
||||
if any([s != "draft" for s in self.mapped("state")]):
|
||||
raise UserError(_("You can only confirm cycle counts in state 'Planned'."))
|
||||
for rec in self:
|
||||
data = rec._prepare_inventory_adjustment()
|
||||
self.env['stock.inventory'].create(data)
|
||||
self.write({'state': 'open'})
|
||||
self.env["stock.inventory"].create(data)
|
||||
self.write({"state": "open"})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_view_inventory(self):
|
||||
action = self.env.ref('stock.action_inventory_form')
|
||||
action = self.env.ref("stock.action_inventory_form")
|
||||
result = action.read()[0]
|
||||
result['context'] = {}
|
||||
adjustment_ids = self.mapped('stock_adjustment_ids').ids
|
||||
result["context"] = {}
|
||||
adjustment_ids = self.mapped("stock_adjustment_ids").ids
|
||||
if len(adjustment_ids) > 1:
|
||||
result['domain'] = [('id', 'in', adjustment_ids)]
|
||||
result["domain"] = [("id", "in", adjustment_ids)]
|
||||
elif len(adjustment_ids) == 1:
|
||||
res = self.env.ref('stock.view_inventory_form', False)
|
||||
result['views'] = [(res and res.id or False, 'form')]
|
||||
result['res_id'] = adjustment_ids and adjustment_ids[0] or False
|
||||
res = self.env.ref("stock.view_inventory_form", False)
|
||||
result["views"] = [(res and res.id or False, "form")]
|
||||
result["res_id"] = adjustment_ids and adjustment_ids[0] or False
|
||||
return result
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
# (http://www.eficent.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
|
||||
class StockCycleCountRule(models.Model):
|
||||
_name = 'stock.cycle.count.rule'
|
||||
_name = "stock.cycle.count.rule"
|
||||
_description = "Stock Cycle Counts Rules"
|
||||
|
||||
@api.multi
|
||||
@@ -19,131 +20,149 @@ class StockCycleCountRule(models.Model):
|
||||
@api.model
|
||||
def _selection_rule_types(self):
|
||||
return [
|
||||
('periodic', _('Periodic')),
|
||||
('turnover', _('Value Turnover')),
|
||||
('accuracy', _('Minimum Accuracy')),
|
||||
('zero', _('Zero Confirmation'))]
|
||||
("periodic", _("Periodic")),
|
||||
("turnover", _("Value Turnover")),
|
||||
("accuracy", _("Minimum Accuracy")),
|
||||
("zero", _("Zero Confirmation")),
|
||||
]
|
||||
|
||||
@api.multi
|
||||
@api.constrains('rule_type', 'warehouse_ids')
|
||||
@api.constrains("rule_type", "warehouse_ids")
|
||||
def _check_zero_rule(self):
|
||||
for rec in self:
|
||||
if rec.rule_type == 'zero' and len(rec.warehouse_ids) > 1:
|
||||
if rec.rule_type == "zero" and len(rec.warehouse_ids) > 1:
|
||||
raise ValidationError(
|
||||
_('Zero confirmation rules can only have one warehouse '
|
||||
'assigned.')
|
||||
_(
|
||||
"Zero confirmation rules can only have one warehouse "
|
||||
"assigned."
|
||||
)
|
||||
)
|
||||
if rec.rule_type == "zero":
|
||||
zero_rule = self.search(
|
||||
[
|
||||
("rule_type", "=", "zero"),
|
||||
("warehouse_ids", "=", rec.warehouse_ids.id),
|
||||
]
|
||||
)
|
||||
if rec.rule_type == 'zero':
|
||||
zero_rule = self.search([
|
||||
('rule_type', '=', 'zero'),
|
||||
('warehouse_ids', '=', rec.warehouse_ids.id)])
|
||||
if len(zero_rule) > 1:
|
||||
raise ValidationError(
|
||||
_('You can only have one zero confirmation rule per '
|
||||
'warehouse.')
|
||||
_(
|
||||
"You can only have one zero confirmation rule per "
|
||||
"warehouse."
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends('rule_type')
|
||||
@api.depends("rule_type")
|
||||
def _compute_rule_description(self):
|
||||
if self.rule_type == 'periodic':
|
||||
self.rule_description = _('Ensures that at least a defined number '
|
||||
'of counts in a given period will '
|
||||
'be run.')
|
||||
elif self.rule_type == 'turnover':
|
||||
self.rule_description = _('Schedules a count every time the total '
|
||||
'turnover of a location exceeds the '
|
||||
'threshold. This considers every '
|
||||
'product going into/out of the location')
|
||||
elif self.rule_type == 'accuracy':
|
||||
self.rule_description = _('Schedules a count every time the '
|
||||
'accuracy of a location goes under a '
|
||||
'given threshold.')
|
||||
elif self.rule_type == 'zero':
|
||||
self.rule_description = _('Perform an Inventory Adjustment every '
|
||||
'time a location in the warehouse runs '
|
||||
'out of stock in order to confirm it is '
|
||||
'truly empty.')
|
||||
if self.rule_type == "periodic":
|
||||
self.rule_description = _(
|
||||
"Ensures that at least a defined number "
|
||||
"of counts in a given period will "
|
||||
"be run."
|
||||
)
|
||||
elif self.rule_type == "turnover":
|
||||
self.rule_description = _(
|
||||
"Schedules a count every time the total "
|
||||
"turnover of a location exceeds the "
|
||||
"threshold. This considers every "
|
||||
"product going into/out of the location"
|
||||
)
|
||||
elif self.rule_type == "accuracy":
|
||||
self.rule_description = _(
|
||||
"Schedules a count every time the "
|
||||
"accuracy of a location goes under a "
|
||||
"given threshold."
|
||||
)
|
||||
elif self.rule_type == "zero":
|
||||
self.rule_description = _(
|
||||
"Perform an Inventory Adjustment every "
|
||||
"time a location in the warehouse runs "
|
||||
"out of stock in order to confirm it is "
|
||||
"truly empty."
|
||||
)
|
||||
else:
|
||||
self.rule_description = _('(No description provided.)')
|
||||
self.rule_description = _("(No description provided.)")
|
||||
|
||||
@api.multi
|
||||
@api.constrains('periodic_qty_per_period', 'periodic_count_period')
|
||||
@api.constrains("periodic_qty_per_period", "periodic_count_period")
|
||||
def _check_negative_periodic(self):
|
||||
for rec in self:
|
||||
if rec.periodic_qty_per_period < 1:
|
||||
raise ValidationError(
|
||||
_('You cannot define a negative or null number of counts '
|
||||
'per period.')
|
||||
_(
|
||||
"You cannot define a negative or null number of counts "
|
||||
"per period."
|
||||
)
|
||||
)
|
||||
if rec.periodic_count_period < 0:
|
||||
raise ValidationError(
|
||||
_('You cannot define a negative period.')
|
||||
)
|
||||
raise ValidationError(_("You cannot define a negative period."))
|
||||
|
||||
@api.onchange('location_ids')
|
||||
@api.onchange("location_ids")
|
||||
def _onchange_locaton_ids(self):
|
||||
"""Get the warehouses for the selected locations."""
|
||||
wh_ids = []
|
||||
for loc in self.location_ids:
|
||||
wh_ids.append(loc.get_warehouse().id)
|
||||
wh_ids = list(set(wh_ids))
|
||||
self.warehouse_ids = self.env['stock.warehouse'].browse(wh_ids)
|
||||
self.warehouse_ids = self.env["stock.warehouse"].browse(wh_ids)
|
||||
|
||||
name = fields.Char(required=True)
|
||||
rule_type = fields.Selection(
|
||||
selection="_selection_rule_types",
|
||||
string='Type of rule', required=True,
|
||||
selection="_selection_rule_types", string="Type of rule", required=True
|
||||
)
|
||||
rule_description = fields.Char(
|
||||
string='Rule Description', compute='_compute_rule_description',
|
||||
string="Rule Description", compute="_compute_rule_description"
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
periodic_qty_per_period = fields.Integer(
|
||||
string='Counts per period', default=1,
|
||||
)
|
||||
periodic_count_period = fields.Integer(string='Period in days')
|
||||
periodic_qty_per_period = fields.Integer(string="Counts per period", default=1)
|
||||
periodic_count_period = fields.Integer(string="Period in days")
|
||||
turnover_inventory_value_threshold = fields.Float(
|
||||
string='Turnover Inventory Value Threshold',
|
||||
string="Turnover Inventory Value Threshold"
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency', string='Currency',
|
||||
compute='_compute_currency_id',
|
||||
comodel_name="res.currency", string="Currency", compute="_compute_currency_id"
|
||||
)
|
||||
accuracy_threshold = fields.Float(
|
||||
string='Minimum Accuracy Threshold', digits=(3, 2),
|
||||
string="Minimum Accuracy Threshold", digits=(3, 2)
|
||||
)
|
||||
apply_in = fields.Selection(
|
||||
string='Apply this rule in:',
|
||||
selection=[('warehouse', 'Selected warehouses'),
|
||||
('location', 'Selected Location Zones.')],
|
||||
default='warehouse',
|
||||
string="Apply this rule in:",
|
||||
selection=[
|
||||
("warehouse", "Selected warehouses"),
|
||||
("location", "Selected Location Zones."),
|
||||
],
|
||||
default="warehouse",
|
||||
)
|
||||
warehouse_ids = fields.Many2many(
|
||||
comodel_name='stock.warehouse',
|
||||
relation='warehouse_cycle_count_rule_rel', column1='rule_id',
|
||||
column2='warehouse_id', string='Warehouses where applied',
|
||||
comodel_name="stock.warehouse",
|
||||
relation="warehouse_cycle_count_rule_rel",
|
||||
column1="rule_id",
|
||||
column2="warehouse_id",
|
||||
string="Warehouses where applied",
|
||||
)
|
||||
location_ids = fields.Many2many(
|
||||
comodel_name='stock.location',
|
||||
relation='location_cycle_count_rule_rel', column1='rule_id',
|
||||
column2='location_id', string='Zones where applied',
|
||||
comodel_name="stock.location",
|
||||
relation="location_cycle_count_rule_rel",
|
||||
column1="rule_id",
|
||||
column2="location_id",
|
||||
string="Zones where applied",
|
||||
)
|
||||
|
||||
def compute_rule(self, locs):
|
||||
if self.rule_type == 'periodic':
|
||||
if self.rule_type == "periodic":
|
||||
proposed_cycle_counts = self._compute_rule_periodic(locs)
|
||||
elif self.rule_type == 'turnover':
|
||||
elif self.rule_type == "turnover":
|
||||
proposed_cycle_counts = self._compute_rule_turnover(locs)
|
||||
elif self.rule_type == 'accuracy':
|
||||
elif self.rule_type == "accuracy":
|
||||
proposed_cycle_counts = self._compute_rule_accuracy(locs)
|
||||
return proposed_cycle_counts
|
||||
|
||||
@api.model
|
||||
def _propose_cycle_count(self, date, location):
|
||||
cycle_count = {
|
||||
'date': fields.Datetime.from_string(date),
|
||||
'location': location,
|
||||
'rule_type': self
|
||||
"date": fields.Datetime.from_string(date),
|
||||
"location": location,
|
||||
"rule_type": self,
|
||||
}
|
||||
return cycle_count
|
||||
|
||||
@@ -151,22 +170,34 @@ class StockCycleCountRule(models.Model):
|
||||
def _compute_rule_periodic(self, locs):
|
||||
cycle_counts = []
|
||||
for loc in locs:
|
||||
latest_inventory_date = self.env['stock.inventory'].search([
|
||||
('location_id', '=', loc.id),
|
||||
('state', 'in', ['confirm', 'done', 'draft'])],
|
||||
order="date desc", limit=1).date
|
||||
latest_inventory_date = (
|
||||
self.env["stock.inventory"]
|
||||
.search(
|
||||
[
|
||||
("location_id", "=", loc.id),
|
||||
("state", "in", ["confirm", "done", "draft"]),
|
||||
],
|
||||
order="date desc",
|
||||
limit=1,
|
||||
)
|
||||
.date
|
||||
)
|
||||
if latest_inventory_date:
|
||||
try:
|
||||
period = self.periodic_count_period / \
|
||||
self.periodic_qty_per_period
|
||||
period = self.periodic_count_period / self.periodic_qty_per_period
|
||||
next_date = fields.Datetime.from_string(
|
||||
latest_inventory_date) + timedelta(days=period)
|
||||
latest_inventory_date
|
||||
) + timedelta(days=period)
|
||||
if next_date < datetime.today():
|
||||
next_date = datetime.today()
|
||||
except Exception as e:
|
||||
raise UserError(
|
||||
_('Error found determining the frequency of periodic '
|
||||
'cycle count rule. %s') % str(e))
|
||||
_(
|
||||
"Error found determining the frequency of periodic "
|
||||
"cycle count rule. %s"
|
||||
)
|
||||
% str(e)
|
||||
)
|
||||
else:
|
||||
next_date = datetime.today()
|
||||
cycle_count = self._propose_cycle_count(next_date, loc)
|
||||
@@ -175,11 +206,15 @@ class StockCycleCountRule(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_turnover_moves(self, location, date):
|
||||
moves = self.env['stock.move'].search([
|
||||
'|', ('location_id', '=', location.id),
|
||||
('location_dest_id', '=', location.id),
|
||||
('date', '>', date),
|
||||
('state', '=', 'done')])
|
||||
moves = self.env["stock.move"].search(
|
||||
[
|
||||
"|",
|
||||
("location_id", "=", location.id),
|
||||
("location_dest_id", "=", location.id),
|
||||
("date", ">", date),
|
||||
("state", "=", "done"),
|
||||
]
|
||||
)
|
||||
return moves
|
||||
|
||||
@api.model
|
||||
@@ -192,9 +227,16 @@ class StockCycleCountRule(models.Model):
|
||||
def _compute_rule_turnover(self, locs):
|
||||
cycle_counts = []
|
||||
for loc in locs:
|
||||
last_inventories = self.env['stock.inventory'].search([
|
||||
('location_id', '=', loc.id),
|
||||
('state', 'in', ['confirm', 'done', 'draft'])]).mapped('date')
|
||||
last_inventories = (
|
||||
self.env["stock.inventory"]
|
||||
.search(
|
||||
[
|
||||
("location_id", "=", loc.id),
|
||||
("state", "in", ["confirm", "done", "draft"]),
|
||||
]
|
||||
)
|
||||
.mapped("date")
|
||||
)
|
||||
if last_inventories:
|
||||
latest_inventory = sorted(last_inventories, reverse=True)[0]
|
||||
moves = self._get_turnover_moves(loc, latest_inventory)
|
||||
@@ -204,16 +246,18 @@ class StockCycleCountRule(models.Model):
|
||||
turnover = self._compute_turnover(m)
|
||||
total_turnover += turnover
|
||||
try:
|
||||
if total_turnover > \
|
||||
self.turnover_inventory_value_threshold:
|
||||
if total_turnover > self.turnover_inventory_value_threshold:
|
||||
next_date = datetime.today()
|
||||
cycle_count = self._propose_cycle_count(next_date,
|
||||
loc)
|
||||
cycle_count = self._propose_cycle_count(next_date, loc)
|
||||
cycle_counts.append(cycle_count)
|
||||
except Exception as e:
|
||||
raise UserError(_(
|
||||
'Error found when comparing turnover with the '
|
||||
'rule threshold. %s') % str(e))
|
||||
raise UserError(
|
||||
_(
|
||||
"Error found when comparing turnover with the "
|
||||
"rule threshold. %s"
|
||||
)
|
||||
% str(e)
|
||||
)
|
||||
else:
|
||||
next_date = datetime.today()
|
||||
cycle_count = self._propose_cycle_count(next_date, loc)
|
||||
|
||||
@@ -2,41 +2,46 @@
|
||||
# (http://www.eficent.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
PERCENT = 100.0
|
||||
|
||||
|
||||
class StockInventory(models.Model):
|
||||
_inherit = 'stock.inventory'
|
||||
_inherit = "stock.inventory"
|
||||
|
||||
@api.multi
|
||||
@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)))
|
||||
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':
|
||||
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)
|
||||
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")
|
||||
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'
|
||||
if inv.cycle_count_id and inv.state == "done":
|
||||
inv.cycle_count_id.state = "done"
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
@@ -54,9 +59,15 @@ class StockInventory(models.Model):
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
for inventory in self:
|
||||
if (inventory.cycle_count_id and 'state' not in vals.keys() and
|
||||
inventory.state == 'draft'):
|
||||
if (
|
||||
inventory.cycle_count_id
|
||||
and "state" not in vals.keys()
|
||||
and inventory.state == "draft"
|
||||
):
|
||||
raise UserError(
|
||||
_('You cannot modify the configuration of an Inventory '
|
||||
'Adjustment related to a Cycle Count.'))
|
||||
_(
|
||||
"You cannot modify the configuration of an Inventory "
|
||||
"Adjustment related to a Cycle Count."
|
||||
)
|
||||
)
|
||||
return super(StockInventory, self).write(vals)
|
||||
|
||||
@@ -3,64 +3,66 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from statistics import mean
|
||||
STATS_PATH = tools.find_in_path('statistics')
|
||||
|
||||
STATS_PATH = tools.find_in_path("statistics")
|
||||
except (ImportError, IOError) as err:
|
||||
_logger.debug(err)
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
_inherit = 'stock.location'
|
||||
_inherit = "stock.location"
|
||||
|
||||
@api.multi
|
||||
def _compute_loc_accuracy(self):
|
||||
for rec in self:
|
||||
history = self.env['stock.inventory'].search([
|
||||
('location_id', '=', rec.id), ('state', '=', 'done')])
|
||||
history = self.env["stock.inventory"].search(
|
||||
[("location_id", "=", rec.id), ("state", "=", "done")]
|
||||
)
|
||||
history = history.sorted(key=lambda r: r.write_date, reverse=True)
|
||||
if history:
|
||||
wh = rec.get_warehouse()
|
||||
if wh.counts_for_accuracy_qty and \
|
||||
len(history) > wh.counts_for_accuracy_qty:
|
||||
if (
|
||||
wh.counts_for_accuracy_qty
|
||||
and len(history) > wh.counts_for_accuracy_qty
|
||||
):
|
||||
rec.loc_accuracy = mean(
|
||||
history[:wh.counts_for_accuracy_qty].mapped(
|
||||
'inventory_accuracy'))
|
||||
history[: wh.counts_for_accuracy_qty].mapped(
|
||||
"inventory_accuracy"
|
||||
)
|
||||
)
|
||||
else:
|
||||
rec.loc_accuracy = mean(
|
||||
history.mapped('inventory_accuracy'))
|
||||
rec.loc_accuracy = mean(history.mapped("inventory_accuracy"))
|
||||
|
||||
zero_confirmation_disabled = fields.Boolean(
|
||||
string='Disable Zero Confirmations',
|
||||
help='Define whether this location will trigger a zero-confirmation '
|
||||
'validation when a rule for its warehouse is defined to perform '
|
||||
'zero-confirmations.',
|
||||
string="Disable Zero Confirmations",
|
||||
help="Define whether this location will trigger a zero-confirmation "
|
||||
"validation when a rule for its warehouse is defined to perform "
|
||||
"zero-confirmations.",
|
||||
)
|
||||
cycle_count_disabled = fields.Boolean(
|
||||
string='Exclude from Cycle Count',
|
||||
help='Define whether the location is going to be cycle counted.',
|
||||
string="Exclude from Cycle Count",
|
||||
help="Define whether the location is going to be cycle counted.",
|
||||
)
|
||||
qty_variance_inventory_threshold = fields.Float(
|
||||
string='Acceptable Inventory Quantity Variance Threshold',
|
||||
string="Acceptable Inventory Quantity Variance Threshold"
|
||||
)
|
||||
loc_accuracy = fields.Float(
|
||||
string='Inventory Accuracy', compute='_compute_loc_accuracy',
|
||||
digits=(3, 2),
|
||||
string="Inventory Accuracy", compute="_compute_loc_accuracy", digits=(3, 2)
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _get_zero_confirmation_domain(self):
|
||||
self.ensure_one()
|
||||
domain = [
|
||||
('location_id', '=', self.id),
|
||||
('quantity', '>', 0.0),
|
||||
]
|
||||
domain = [("location_id", "=", self.id), ("quantity", ">", 0.0)]
|
||||
return domain
|
||||
|
||||
@api.multi
|
||||
@@ -68,13 +70,14 @@ class StockLocation(models.Model):
|
||||
for rec in self:
|
||||
if not rec.zero_confirmation_disabled:
|
||||
wh = rec.get_warehouse()
|
||||
rule_model = self.env['stock.cycle.count.rule']
|
||||
zero_rule = rule_model.search([
|
||||
('rule_type', '=', 'zero'),
|
||||
('warehouse_ids', '=', wh.id)])
|
||||
rule_model = self.env["stock.cycle.count.rule"]
|
||||
zero_rule = rule_model.search(
|
||||
[("rule_type", "=", "zero"), ("warehouse_ids", "=", wh.id)]
|
||||
)
|
||||
if zero_rule:
|
||||
quants = self.env['stock.quant'].search(
|
||||
rec._get_zero_confirmation_domain())
|
||||
quants = self.env["stock.quant"].search(
|
||||
rec._get_zero_confirmation_domain()
|
||||
)
|
||||
if not quants:
|
||||
rec.create_zero_confirmation_cycle_count()
|
||||
|
||||
@@ -83,30 +86,41 @@ class StockLocation(models.Model):
|
||||
self.ensure_one()
|
||||
date = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
wh_id = self.get_warehouse().id
|
||||
date_horizon = self.get_warehouse().get_horizon_date().strftime(
|
||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
counts_planned = self.env['stock.cycle.count'].search([
|
||||
('date_deadline', '<', date_horizon), ('state', '=', 'draft'),
|
||||
('location_id', '=', self.id)])
|
||||
date_horizon = (
|
||||
self.get_warehouse()
|
||||
.get_horizon_date()
|
||||
.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
)
|
||||
counts_planned = self.env["stock.cycle.count"].search(
|
||||
[
|
||||
("date_deadline", "<", date_horizon),
|
||||
("state", "=", "draft"),
|
||||
("location_id", "=", self.id),
|
||||
]
|
||||
)
|
||||
if counts_planned:
|
||||
counts_planned.write({'state': 'cancelled'})
|
||||
rule = self.env['stock.cycle.count.rule'].search([
|
||||
('rule_type', '=', 'zero'), ('warehouse_ids', '=', wh_id)])
|
||||
self.env['stock.cycle.count'].create({
|
||||
'date_deadline': date,
|
||||
'location_id': self.id,
|
||||
'cycle_count_rule_id': rule.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
counts_planned.write({"state": "cancelled"})
|
||||
rule = self.env["stock.cycle.count.rule"].search(
|
||||
[("rule_type", "=", "zero"), ("warehouse_ids", "=", wh_id)]
|
||||
)
|
||||
self.env["stock.cycle.count"].create(
|
||||
{
|
||||
"date_deadline": date,
|
||||
"location_id": self.id,
|
||||
"cycle_count_rule_id": rule.id,
|
||||
"state": "draft",
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_accuracy_stats(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock_cycle_count.act_accuracy_stats')
|
||||
action = self.env.ref("stock_cycle_count.act_accuracy_stats")
|
||||
result = action.read()[0]
|
||||
result['context'] = {"search_default_location_id": self.id}
|
||||
new_domain = result['domain'][:-1] + \
|
||||
", ('location_id', 'child_of', active_ids)]"
|
||||
result['domain'] = new_domain
|
||||
result["context"] = {"search_default_location_id": self.id}
|
||||
new_domain = (
|
||||
result["domain"][:-1] + ", ('location_id', 'child_of', active_ids)]"
|
||||
)
|
||||
result["domain"] = new_domain
|
||||
return result
|
||||
|
||||
@@ -7,7 +7,7 @@ from odoo import api, models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
_inherit = "stock.move"
|
||||
|
||||
@api.multi
|
||||
def _action_done(self):
|
||||
|
||||
@@ -2,31 +2,34 @@
|
||||
# (http://www.eficent.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
_inherit = "stock.warehouse"
|
||||
|
||||
cycle_count_rule_ids = fields.Many2many(
|
||||
comodel_name='stock.cycle.count.rule',
|
||||
relation='warehouse_cycle_count_rule_rel',
|
||||
column1='warehouse_id',
|
||||
column2='rule_id',
|
||||
string='Cycle Count Rules')
|
||||
comodel_name="stock.cycle.count.rule",
|
||||
relation="warehouse_cycle_count_rule_rel",
|
||||
column1="warehouse_id",
|
||||
column2="rule_id",
|
||||
string="Cycle Count Rules",
|
||||
)
|
||||
cycle_count_planning_horizon = fields.Integer(
|
||||
string='Cycle Count Planning Horizon (in days)',
|
||||
help='Cycle Count planning horizon in days. Only the counts inside '
|
||||
'the horizon will be created.')
|
||||
string="Cycle Count Planning Horizon (in days)",
|
||||
help="Cycle Count planning horizon in days. Only the counts inside "
|
||||
"the horizon will be created.",
|
||||
)
|
||||
counts_for_accuracy_qty = fields.Integer(
|
||||
string='Inventories for location accuracy calculation',
|
||||
string="Inventories for location accuracy calculation",
|
||||
default=1,
|
||||
help='Number of latest inventories used to calculate location '
|
||||
'accuracy')
|
||||
help="Number of latest inventories used to calculate location " "accuracy",
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def get_horizon_date(self):
|
||||
@@ -37,40 +40,42 @@ class StockWarehouse(models.Model):
|
||||
return date_horizon
|
||||
|
||||
@api.model
|
||||
def _get_cycle_count_locations_search_domain(
|
||||
self, parent):
|
||||
domain = [('parent_path', '=like', parent.parent_path + '%'),
|
||||
('cycle_count_disabled', '=', False)]
|
||||
def _get_cycle_count_locations_search_domain(self, parent):
|
||||
domain = [
|
||||
("parent_path", "=like", parent.parent_path + "%"),
|
||||
("cycle_count_disabled", "=", False),
|
||||
]
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
def _search_cycle_count_locations(self, rule):
|
||||
locations = self.env['stock.location']
|
||||
if rule.apply_in == 'warehouse':
|
||||
locations = self.env['stock.location'].search(
|
||||
self._get_cycle_count_locations_search_domain(
|
||||
self.view_location_id))
|
||||
elif rule.apply_in == 'location':
|
||||
locations = self.env["stock.location"]
|
||||
if rule.apply_in == "warehouse":
|
||||
locations = self.env["stock.location"].search(
|
||||
self._get_cycle_count_locations_search_domain(self.view_location_id)
|
||||
)
|
||||
elif rule.apply_in == "location":
|
||||
for loc in rule.location_ids:
|
||||
locations += self.env['stock.location'].search(
|
||||
self._get_cycle_count_locations_search_domain(loc))
|
||||
locations += self.env["stock.location"].search(
|
||||
self._get_cycle_count_locations_search_domain(loc)
|
||||
)
|
||||
return locations
|
||||
|
||||
@api.multi
|
||||
def _cycle_count_rules_to_compute(self):
|
||||
self.ensure_one()
|
||||
rules = self.env['stock.cycle.count.rule'].search([
|
||||
('rule_type', '!=', 'zero'), ('warehouse_ids', 'in', self.ids)])
|
||||
rules = self.env["stock.cycle.count.rule"].search(
|
||||
[("rule_type", "!=", "zero"), ("warehouse_ids", "in", self.ids)]
|
||||
)
|
||||
return rules
|
||||
|
||||
@api.model
|
||||
def _prepare_cycle_count(self, cycle_count_proposed):
|
||||
return {
|
||||
'date_deadline': cycle_count_proposed['date'],
|
||||
'location_id': cycle_count_proposed['location'].id,
|
||||
'cycle_count_rule_id': cycle_count_proposed[
|
||||
'rule_type'].id,
|
||||
'state': 'draft'
|
||||
"date_deadline": cycle_count_proposed["date"],
|
||||
"location_id": cycle_count_proposed["location"].id,
|
||||
"cycle_count_rule_id": cycle_count_proposed["rule_type"].id,
|
||||
"state": "draft",
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@@ -86,43 +91,49 @@ class StockWarehouse(models.Model):
|
||||
if locations:
|
||||
proposed_cycle_counts.extend(rule.compute_rule(locations))
|
||||
if proposed_cycle_counts:
|
||||
locations = list(set([d['location'] for d in
|
||||
proposed_cycle_counts]))
|
||||
locations = list({d["location"] for d in proposed_cycle_counts})
|
||||
for loc in locations:
|
||||
proposed_for_loc = list(filter(
|
||||
lambda x: x['location'] == loc, proposed_cycle_counts))
|
||||
earliest_date = min([d['date'] for d in proposed_for_loc])
|
||||
cycle_count_proposed = list(filter(
|
||||
lambda x: x['date'] == earliest_date,
|
||||
proposed_for_loc))[0]
|
||||
domain = [('location_id', '=', loc.id),
|
||||
('state', 'in', ['draft'])]
|
||||
existing_cycle_counts = self.env[
|
||||
'stock.cycle.count'].search(domain)
|
||||
proposed_for_loc = list(
|
||||
filter(lambda x: x["location"] == loc, proposed_cycle_counts)
|
||||
)
|
||||
earliest_date = min([d["date"] for d in proposed_for_loc])
|
||||
cycle_count_proposed = list(
|
||||
filter(lambda x: x["date"] == earliest_date, proposed_for_loc)
|
||||
)[0]
|
||||
domain = [("location_id", "=", loc.id), ("state", "in", ["draft"])]
|
||||
existing_cycle_counts = self.env["stock.cycle.count"].search(domain)
|
||||
if existing_cycle_counts:
|
||||
existing_earliest_date = sorted(
|
||||
existing_cycle_counts.mapped('date_deadline'))[0]
|
||||
existing_cycle_counts.mapped("date_deadline")
|
||||
)[0]
|
||||
existing_earliest_date = fields.Date.from_string(
|
||||
existing_earliest_date)
|
||||
existing_earliest_date
|
||||
)
|
||||
cycle_count_proposed_date = fields.Date.from_string(
|
||||
cycle_count_proposed['date'])
|
||||
if (cycle_count_proposed_date <
|
||||
existing_earliest_date):
|
||||
cc_to_update = existing_cycle_counts.search([
|
||||
('date_deadline', '=', existing_earliest_date)
|
||||
])
|
||||
cc_to_update.write({
|
||||
'date_deadline': cycle_count_proposed_date,
|
||||
'cycle_count_rule_id': cycle_count_proposed[
|
||||
'rule_type'].id,
|
||||
})
|
||||
delta = (fields.Datetime.from_string(
|
||||
cycle_count_proposed['date']) - datetime.today())
|
||||
if not existing_cycle_counts and \
|
||||
delta.days < rec.cycle_count_planning_horizon:
|
||||
cc_vals = self._prepare_cycle_count(
|
||||
cycle_count_proposed)
|
||||
self.env['stock.cycle.count'].create(cc_vals)
|
||||
cycle_count_proposed["date"]
|
||||
)
|
||||
if cycle_count_proposed_date < existing_earliest_date:
|
||||
cc_to_update = existing_cycle_counts.search(
|
||||
[("date_deadline", "=", existing_earliest_date)]
|
||||
)
|
||||
cc_to_update.write(
|
||||
{
|
||||
"date_deadline": cycle_count_proposed_date,
|
||||
"cycle_count_rule_id": cycle_count_proposed[
|
||||
"rule_type"
|
||||
].id,
|
||||
}
|
||||
)
|
||||
delta = (
|
||||
fields.Datetime.from_string(cycle_count_proposed["date"])
|
||||
- datetime.today()
|
||||
)
|
||||
if (
|
||||
not existing_cycle_counts
|
||||
and delta.days < rec.cycle_count_planning_horizon
|
||||
):
|
||||
cc_vals = self._prepare_cycle_count(cycle_count_proposed)
|
||||
self.env["stock.cycle.count"].create(cc_vals)
|
||||
|
||||
@api.model
|
||||
def cron_cycle_count(self):
|
||||
@@ -131,8 +142,7 @@ class StockWarehouse(models.Model):
|
||||
whs = self.search([])
|
||||
whs.action_compute_cycle_count_rules()
|
||||
except Exception as e:
|
||||
_logger.info(
|
||||
"Error while running stock_cycle_count cron job: %s", str(e))
|
||||
_logger.info("Error while running stock_cycle_count cron job: %s", str(e))
|
||||
raise
|
||||
_logger.info("stock_cycle_count cron job ended.")
|
||||
return True
|
||||
|
||||
@@ -11,9 +11,11 @@ class LocationAccuracyReport(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def _get_inventory_domain(self, loc_id, exclude_sublocation=True):
|
||||
return [('location_id', '=', loc_id),
|
||||
('exclude_sublocation', '=', exclude_sublocation),
|
||||
('state', '=', 'done')]
|
||||
return [
|
||||
("location_id", "=", loc_id),
|
||||
("exclude_sublocation", "=", exclude_sublocation),
|
||||
("state", "=", "done"),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _get_location_data(self, locations):
|
||||
@@ -29,10 +31,5 @@ class LocationAccuracyReport(models.AbstractModel):
|
||||
report_obj = self.env["report"]
|
||||
locs = self.env["stock.location"].browse(self._ids)
|
||||
data = self._get_location_data(locs)
|
||||
docargs = {
|
||||
"doc_ids": locs._ids,
|
||||
"docs": locs,
|
||||
"data": data,
|
||||
}
|
||||
return report_obj.render(
|
||||
"stock_cycle_count.stock_location_accuracy", docargs)
|
||||
docargs = {"doc_ids": locs._ids, "docs": locs, "data": data}
|
||||
return report_obj.render("stock_cycle_count.stock_location_accuracy", docargs)
|
||||
|
||||
@@ -1,136 +1,139 @@
|
||||
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
# (http://www.eficent.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from odoo.tests import common
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestStockCycleCount(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestStockCycleCount, self).setUp()
|
||||
self.res_users_model = self.env['res.users']
|
||||
self.cycle_count_model = self.env['stock.cycle.count']
|
||||
self.stock_cycle_count_rule_model = self.env['stock.cycle.count.rule']
|
||||
self.inventory_model = self.env['stock.inventory']
|
||||
self.stock_location_model = self.env['stock.location']
|
||||
self.stock_move_model = self.env['stock.move']
|
||||
self.stock_warehouse_model = self.env['stock.warehouse']
|
||||
self.product_model = self.env['product.product']
|
||||
self.quant_model = self.env['stock.quant']
|
||||
self.move_model = self.env['stock.move']
|
||||
self.res_users_model = self.env["res.users"]
|
||||
self.cycle_count_model = self.env["stock.cycle.count"]
|
||||
self.stock_cycle_count_rule_model = self.env["stock.cycle.count.rule"]
|
||||
self.inventory_model = self.env["stock.inventory"]
|
||||
self.stock_location_model = self.env["stock.location"]
|
||||
self.stock_move_model = self.env["stock.move"]
|
||||
self.stock_warehouse_model = self.env["stock.warehouse"]
|
||||
self.product_model = self.env["product.product"]
|
||||
self.quant_model = self.env["stock.quant"]
|
||||
self.move_model = self.env["stock.move"]
|
||||
|
||||
self.company = self.env.ref('base.main_company')
|
||||
self.partner = self.env.ref('base.res_partner_1')
|
||||
self.g_stock_manager = self.env.ref('stock.group_stock_manager')
|
||||
self.g_stock_user = self.env.ref('stock.group_stock_user')
|
||||
self.company = self.env.ref("base.main_company")
|
||||
self.partner = self.env.ref("base.res_partner_1")
|
||||
self.g_stock_manager = self.env.ref("stock.group_stock_manager")
|
||||
self.g_stock_user = self.env.ref("stock.group_stock_user")
|
||||
|
||||
# Create users:
|
||||
self.manager = self._create_user(
|
||||
'user_1', [self.g_stock_manager], self.company).id
|
||||
self.user = self._create_user(
|
||||
'user_2', [self.g_stock_user], self.company).id
|
||||
"user_1", [self.g_stock_manager], self.company
|
||||
).id
|
||||
self.user = self._create_user("user_2", [self.g_stock_user], self.company).id
|
||||
|
||||
# Create warehouses:
|
||||
self.big_wh = self.stock_warehouse_model.create({
|
||||
'name': 'BIG',
|
||||
'code': 'B',
|
||||
'cycle_count_planning_horizon': 30})
|
||||
self.small_wh = self.stock_warehouse_model.create({
|
||||
'name': 'SMALL', 'code': 'S'})
|
||||
self.big_wh = self.stock_warehouse_model.create(
|
||||
{"name": "BIG", "code": "B", "cycle_count_planning_horizon": 30}
|
||||
)
|
||||
self.small_wh = self.stock_warehouse_model.create(
|
||||
{"name": "SMALL", "code": "S"}
|
||||
)
|
||||
|
||||
# Create rules:
|
||||
self.rule_periodic = \
|
||||
self._create_stock_cycle_count_rule_periodic(
|
||||
self.manager, 'rule_1', [2, 7])
|
||||
self.rule_turnover = \
|
||||
self._create_stock_cycle_count_rule_turnover(
|
||||
self.manager, 'rule_2', [100])
|
||||
self.rule_accuracy = \
|
||||
self._create_stock_cycle_count_rule_accuracy(
|
||||
self.manager, 'rule_3', [5], self.big_wh.view_location_id.ids)
|
||||
self.rule_periodic = self._create_stock_cycle_count_rule_periodic(
|
||||
self.manager, "rule_1", [2, 7]
|
||||
)
|
||||
self.rule_turnover = self._create_stock_cycle_count_rule_turnover(
|
||||
self.manager, "rule_2", [100]
|
||||
)
|
||||
self.rule_accuracy = self._create_stock_cycle_count_rule_accuracy(
|
||||
self.manager, "rule_3", [5], self.big_wh.view_location_id.ids
|
||||
)
|
||||
self.zero_rule = self._create_stock_cycle_count_rule_zero(
|
||||
self.manager, 'rule_4')
|
||||
self.manager, "rule_4"
|
||||
)
|
||||
|
||||
# Configure warehouses:
|
||||
self.rule_ids = [
|
||||
self.rule_periodic.id,
|
||||
self.rule_turnover.id,
|
||||
self.rule_accuracy.id,
|
||||
self.zero_rule.id]
|
||||
self.big_wh.write({
|
||||
'cycle_count_rule_ids': [(6, 0, self.rule_ids)]
|
||||
})
|
||||
self.zero_rule.id,
|
||||
]
|
||||
self.big_wh.write({"cycle_count_rule_ids": [(6, 0, self.rule_ids)]})
|
||||
|
||||
# Create a location:
|
||||
self.count_loc = self.stock_location_model.create({
|
||||
'name': 'Place',
|
||||
'usage': 'production'
|
||||
})
|
||||
self.count_loc = self.stock_location_model.create(
|
||||
{"name": "Place", "usage": "production"}
|
||||
)
|
||||
self.stock_location_model._parent_store_compute()
|
||||
|
||||
# Create a cycle count:
|
||||
self.cycle_count_1 = self.cycle_count_model.sudo(self.manager).create({
|
||||
'name': 'Test cycle count',
|
||||
'cycle_count_rule_id': self.rule_periodic.id,
|
||||
'location_id': self.count_loc.id,
|
||||
})
|
||||
self.cycle_count_1 = self.cycle_count_model.sudo(self.manager).create(
|
||||
{
|
||||
"name": "Test cycle count",
|
||||
"cycle_count_rule_id": self.rule_periodic.id,
|
||||
"location_id": self.count_loc.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create a product:
|
||||
self.product1 = self.product_model.create({
|
||||
'name': 'Test Product 1',
|
||||
'type': 'product',
|
||||
'default_code': 'PROD1',
|
||||
})
|
||||
self.product1 = self.product_model.create(
|
||||
{"name": "Test Product 1", "type": "product", "default_code": "PROD1"}
|
||||
)
|
||||
|
||||
def _create_user(self, login, groups, company):
|
||||
group_ids = [group.id for group in groups]
|
||||
user = self.res_users_model.create({
|
||||
'name': login,
|
||||
'login': login,
|
||||
'email': 'example@yourcompany.com',
|
||||
'company_id': company.id,
|
||||
'company_ids': [(4, company.id)],
|
||||
'groups_id': [(6, 0, group_ids)]
|
||||
})
|
||||
user = self.res_users_model.create(
|
||||
{
|
||||
"name": login,
|
||||
"login": login,
|
||||
"email": "example@yourcompany.com",
|
||||
"company_id": company.id,
|
||||
"company_ids": [(4, company.id)],
|
||||
"groups_id": [(6, 0, group_ids)],
|
||||
}
|
||||
)
|
||||
return user
|
||||
|
||||
def _create_stock_cycle_count_rule_periodic(self, uid, name, values):
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create({
|
||||
'name': name,
|
||||
'rule_type': 'periodic',
|
||||
'periodic_qty_per_period': values[0],
|
||||
'periodic_count_period': values[1],
|
||||
})
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create(
|
||||
{
|
||||
"name": name,
|
||||
"rule_type": "periodic",
|
||||
"periodic_qty_per_period": values[0],
|
||||
"periodic_count_period": values[1],
|
||||
}
|
||||
)
|
||||
return rule
|
||||
|
||||
def _create_stock_cycle_count_rule_turnover(self, uid, name, values):
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create({
|
||||
'name': name,
|
||||
'rule_type': 'turnover',
|
||||
'turnover_inventory_value_threshold': values[0],
|
||||
})
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create(
|
||||
{
|
||||
"name": name,
|
||||
"rule_type": "turnover",
|
||||
"turnover_inventory_value_threshold": values[0],
|
||||
}
|
||||
)
|
||||
return rule
|
||||
|
||||
def _create_stock_cycle_count_rule_accuracy(
|
||||
self, uid, name, values, zone_ids):
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create({
|
||||
'name': name,
|
||||
'rule_type': 'accuracy',
|
||||
'accuracy_threshold': values[0],
|
||||
'apply_in': 'location',
|
||||
'location_ids': [(6, 0, zone_ids)],
|
||||
})
|
||||
def _create_stock_cycle_count_rule_accuracy(self, uid, name, values, zone_ids):
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create(
|
||||
{
|
||||
"name": name,
|
||||
"rule_type": "accuracy",
|
||||
"accuracy_threshold": values[0],
|
||||
"apply_in": "location",
|
||||
"location_ids": [(6, 0, zone_ids)],
|
||||
}
|
||||
)
|
||||
return rule
|
||||
|
||||
def _create_stock_cycle_count_rule_zero(self, uid, name):
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create({
|
||||
'name': name,
|
||||
'rule_type': 'zero',
|
||||
})
|
||||
rule = self.stock_cycle_count_rule_model.sudo(uid).create(
|
||||
{"name": name, "rule_type": "zero"}
|
||||
)
|
||||
return rule
|
||||
|
||||
def test_cycle_count_planner(self):
|
||||
@@ -141,113 +144,127 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
for rule in self.big_wh.cycle_count_rule_ids:
|
||||
locs += wh._search_cycle_count_locations(rule)
|
||||
locs = locs.exists() # remove duplicated locations.
|
||||
counts = self.cycle_count_model.search([
|
||||
('location_id', 'in', locs.ids)])
|
||||
self.assertFalse(
|
||||
counts, 'Existing cycle counts before execute planner.')
|
||||
counts = self.cycle_count_model.search([("location_id", "in", locs.ids)])
|
||||
self.assertFalse(counts, "Existing cycle counts before execute planner.")
|
||||
date_pre_existing_cc = datetime.today() + timedelta(days=30)
|
||||
loc = locs.filtered(lambda l: l.usage != 'view')[0]
|
||||
pre_existing_count = self.cycle_count_model.create({
|
||||
'name': 'To be cancelled when running cron job.',
|
||||
'cycle_count_rule_id': self.rule_periodic.id,
|
||||
'location_id': loc.id,
|
||||
'date_deadline': date_pre_existing_cc
|
||||
})
|
||||
self.assertEqual(pre_existing_count.state, 'draft',
|
||||
'Testing data not generated properly.')
|
||||
loc = locs.filtered(lambda l: l.usage != "view")[0]
|
||||
pre_existing_count = self.cycle_count_model.create(
|
||||
{
|
||||
"name": "To be cancelled when running cron job.",
|
||||
"cycle_count_rule_id": self.rule_periodic.id,
|
||||
"location_id": loc.id,
|
||||
"date_deadline": date_pre_existing_cc,
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
pre_existing_count.state, "draft", "Testing data not generated properly."
|
||||
)
|
||||
date = datetime.today() - timedelta(days=1)
|
||||
self.inventory_model.create({
|
||||
'name': 'Pre-existing inventory',
|
||||
'location_id': loc.id,
|
||||
'date': date
|
||||
})
|
||||
self.quant_model.create({
|
||||
'product_id': self.product1.id,
|
||||
'location_id': self.count_loc.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
move1 = self.stock_move_model.create({
|
||||
'name': 'Pre-existing move',
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product1.uom_id.id,
|
||||
'location_id': self.count_loc.id,
|
||||
'location_dest_id': loc.id
|
||||
})
|
||||
self.inventory_model.create(
|
||||
{"name": "Pre-existing inventory", "location_id": loc.id, "date": date}
|
||||
)
|
||||
self.quant_model.create(
|
||||
{
|
||||
"product_id": self.product1.id,
|
||||
"location_id": self.count_loc.id,
|
||||
"quantity": 1.0,
|
||||
}
|
||||
)
|
||||
move1 = self.stock_move_model.create(
|
||||
{
|
||||
"name": "Pre-existing move",
|
||||
"product_id": self.product1.id,
|
||||
"product_uom_qty": 1.0,
|
||||
"product_uom": self.product1.uom_id.id,
|
||||
"location_id": self.count_loc.id,
|
||||
"location_dest_id": loc.id,
|
||||
}
|
||||
)
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move1.move_line_ids[0].qty_done = 1.0
|
||||
move1._action_done()
|
||||
wh.cron_cycle_count()
|
||||
self.assertNotEqual(pre_existing_count.date_deadline,
|
||||
date_pre_existing_cc,
|
||||
'Date of pre-existing cycle counts has not been '
|
||||
'updated.')
|
||||
counts = self.cycle_count_model.search([
|
||||
('location_id', 'in', locs.ids)])
|
||||
self.assertTrue(counts, 'Cycle counts not planned')
|
||||
self.assertNotEqual(
|
||||
pre_existing_count.date_deadline,
|
||||
date_pre_existing_cc,
|
||||
"Date of pre-existing cycle counts has not been " "updated.",
|
||||
)
|
||||
counts = self.cycle_count_model.search([("location_id", "in", locs.ids)])
|
||||
self.assertTrue(counts, "Cycle counts not planned")
|
||||
# Zero-confirmations:
|
||||
count = self.cycle_count_model.search([
|
||||
('location_id', '=', loc.id),
|
||||
('cycle_count_rule_id', '=', self.zero_rule.id)])
|
||||
self.assertFalse(
|
||||
count, 'Unexpected zero confirmation.')
|
||||
move2 = self.move_model.create({
|
||||
'name': 'make the locations to run out of stock.',
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product1.uom_id.id,
|
||||
'location_id': loc.id,
|
||||
'location_dest_id': self.count_loc.id
|
||||
})
|
||||
count = self.cycle_count_model.search(
|
||||
[
|
||||
("location_id", "=", loc.id),
|
||||
("cycle_count_rule_id", "=", self.zero_rule.id),
|
||||
]
|
||||
)
|
||||
self.assertFalse(count, "Unexpected zero confirmation.")
|
||||
move2 = self.move_model.create(
|
||||
{
|
||||
"name": "make the locations to run out of stock.",
|
||||
"product_id": self.product1.id,
|
||||
"product_uom_qty": 1.0,
|
||||
"product_uom": self.product1.uom_id.id,
|
||||
"location_id": loc.id,
|
||||
"location_dest_id": self.count_loc.id,
|
||||
}
|
||||
)
|
||||
move2._action_confirm()
|
||||
move2._action_assign()
|
||||
move2.move_line_ids[0].qty_done = 1.0
|
||||
move2._action_done()
|
||||
count = self.cycle_count_model.search([
|
||||
('location_id', '=', loc.id),
|
||||
('cycle_count_rule_id', '=', self.zero_rule.id)])
|
||||
self.assertTrue(
|
||||
count, 'Zero confirmation not being created.')
|
||||
count = self.cycle_count_model.search(
|
||||
[
|
||||
("location_id", "=", loc.id),
|
||||
("cycle_count_rule_id", "=", self.zero_rule.id),
|
||||
]
|
||||
)
|
||||
self.assertTrue(count, "Zero confirmation not being created.")
|
||||
|
||||
def test_cycle_count_workflow(self):
|
||||
"""Tests workflow."""
|
||||
self.cycle_count_1.action_create_inventory_adjustment()
|
||||
inventory = self.inventory_model.search([
|
||||
('cycle_count_id', '=', self.cycle_count_1.id)])
|
||||
self.assertTrue(inventory, 'Inventory not created.')
|
||||
inventory = self.inventory_model.search(
|
||||
[("cycle_count_id", "=", self.cycle_count_1.id)]
|
||||
)
|
||||
self.assertTrue(inventory, "Inventory not created.")
|
||||
inventory.action_start()
|
||||
inventory.action_validate()
|
||||
self.assertEqual(self.cycle_count_1.state, 'done',
|
||||
'Cycle count not set as done.')
|
||||
self.assertEqual(
|
||||
self.cycle_count_1.state, "done", "Cycle count not set as done."
|
||||
)
|
||||
self.cycle_count_1.do_cancel()
|
||||
self.assertEqual(self.cycle_count_1.state, 'cancelled',
|
||||
'Cycle count not set as cancelled.')
|
||||
self.assertEqual(
|
||||
self.cycle_count_1.state, "cancelled", "Cycle count not set as cancelled."
|
||||
)
|
||||
|
||||
def test_view_methods(self):
|
||||
"""Tests the methods used to handle views."""
|
||||
self.cycle_count_1.action_create_inventory_adjustment()
|
||||
self.cycle_count_1.action_view_inventory()
|
||||
inv_count = self.cycle_count_1.inventory_adj_count
|
||||
self.assertEqual(inv_count, 1,
|
||||
'View method failing.')
|
||||
rules = [self.rule_periodic,
|
||||
self.rule_turnover,
|
||||
self.rule_accuracy,
|
||||
self.zero_rule]
|
||||
self.assertEqual(inv_count, 1, "View method failing.")
|
||||
rules = [
|
||||
self.rule_periodic,
|
||||
self.rule_turnover,
|
||||
self.rule_accuracy,
|
||||
self.zero_rule,
|
||||
]
|
||||
for r in rules:
|
||||
r._compute_rule_description()
|
||||
self.assertTrue(r.rule_description, 'No description provided')
|
||||
self.assertTrue(r.rule_description, "No description provided")
|
||||
self.rule_accuracy._onchange_locaton_ids()
|
||||
self.assertEqual(self.rule_accuracy.warehouse_ids.ids, self.big_wh.ids,
|
||||
'Rules defined for zones are not getting the right '
|
||||
'warehouse.')
|
||||
self.assertEqual(
|
||||
self.rule_accuracy.warehouse_ids.ids,
|
||||
self.big_wh.ids,
|
||||
"Rules defined for zones are not getting the right " "warehouse.",
|
||||
)
|
||||
|
||||
def test_user_security(self):
|
||||
"""Tests user rights."""
|
||||
with self.assertRaises(AccessError):
|
||||
self._create_stock_cycle_count_rule_periodic(
|
||||
self.user, 'rule_1b', [2, 7])
|
||||
self._create_stock_cycle_count_rule_periodic(self.user, "rule_1b", [2, 7])
|
||||
with self.assertRaises(AccessError):
|
||||
self.cycle_count_1.sudo(self.user).unlink()
|
||||
|
||||
@@ -255,22 +272,20 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
"""Tests the constrains for the periodic rules."""
|
||||
# constrain: periodic_qty_per_period < 1
|
||||
with self.assertRaises(ValidationError):
|
||||
self._create_stock_cycle_count_rule_periodic(
|
||||
self.manager, 'rule_0', [0, 0])
|
||||
self._create_stock_cycle_count_rule_periodic(self.manager, "rule_0", [0, 0])
|
||||
# constrain: periodic_count_period < 0
|
||||
with self.assertRaises(ValidationError):
|
||||
self._create_stock_cycle_count_rule_periodic(
|
||||
self.manager, 'rule_0', [1, -1])
|
||||
self.manager, "rule_0", [1, -1]
|
||||
)
|
||||
|
||||
def test_rule_zero_constrains(self):
|
||||
"""Tests the constrains for the zero-confirmation rule: it might
|
||||
only exist one zero confirmation rule per warehouse and have just
|
||||
one warehouse assigned.
|
||||
"""
|
||||
zero2 = self._create_stock_cycle_count_rule_zero(
|
||||
self.manager, 'zero_rule_2')
|
||||
zero2 = self._create_stock_cycle_count_rule_zero(self.manager, "zero_rule_2")
|
||||
with self.assertRaises(ValidationError):
|
||||
zero2.warehouse_ids = [(4, self.big_wh.id)]
|
||||
with self.assertRaises(ValidationError):
|
||||
self.zero_rule.warehouse_ids = [
|
||||
(4, self.small_wh.id)]
|
||||
self.zero_rule.warehouse_ids = [(4, self.small_wh.id)]
|
||||
|
||||
Reference in New Issue
Block a user