diff --git a/stock_cycle_count/models/stock_cycle_count.py b/stock_cycle_count/models/stock_cycle_count.py index 36999b62b..eebd29be0 100644 --- a/stock_cycle_count/models/stock_cycle_count.py +++ b/stock_cycle_count/models/stock_cycle_count.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# Copyright 2017-18 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). @@ -13,7 +13,7 @@ class StockCycleCount(models.Model): _inherit = 'mail.thread' @api.multi - def _count_inventory_adj(self): + def _compute_inventory_adj_count(self): for rec in self: rec.inventory_adj_count = len(rec.stock_adjustment_ids) @@ -53,7 +53,8 @@ class StockCycleCount(models.Model): inverse_name='cycle_count_id', string='Inventory Adjustment', track_visibility='onchange') - inventory_adj_count = fields.Integer(compute='_count_inventory_adj') + inventory_adj_count = fields.Integer( + compute='_compute_inventory_adj_count') company_id = fields.Many2one( comodel_name='res.company', string='Company', required=True, default=_company_get, readonly=True) @@ -62,8 +63,9 @@ class StockCycleCount(models.Model): def do_cancel(self): self.write({'state': 'cancelled'}) - @api.model + @api.multi def _prepare_inventory_adjustment(self): + self.ensure_one() return { 'name': 'INV/{}'.format(self.name), 'cycle_count_id': self.id, diff --git a/stock_cycle_count/models/stock_cycle_count_rule.py b/stock_cycle_count/models/stock_cycle_count_rule.py index 96a551a3f..4b6879dd1 100644 --- a/stock_cycle_count/models/stock_cycle_count_rule.py +++ b/stock_cycle_count/models/stock_cycle_count_rule.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# Copyright 2017-18 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 import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from datetime import timedelta, datetime @@ -13,9 +13,10 @@ class StockCycleCountRule(models.Model): _name = 'stock.cycle.count.rule' _description = "Stock Cycle Counts Rules" - @api.one - def _compute_currency(self): - self.currency_id = self.env.user.company_id.currency_id + @api.multi + def _compute_currency_id(self): + for rec in self: + rec.currency_id = self.env.user.company_id.currency_id @api.model def _selection_rule_types(self): @@ -25,26 +26,27 @@ class StockCycleCountRule(models.Model): ('accuracy', _('Minimum Accuracy')), ('zero', _('Zero Confirmation'))] - @api.one + @api.multi @api.constrains('rule_type', 'warehouse_ids') def _check_zero_rule(self): - if self.rule_type == 'zero' and len(self.warehouse_ids) > 1: - raise UserError( - _('Zero confirmation rules can only have one warehouse ' - 'assigned.') - ) - if self.rule_type == 'zero': - zero_rule = self.search([ - ('rule_type', '=', 'zero'), - ('warehouse_ids', '=', self.warehouse_ids.id)]) - if len(zero_rule) > 1: - raise UserError( - _('You can only have one zero confirmation rule per ' - 'warehouse.') + for rec in self: + if rec.rule_type == 'zero' and len(rec.warehouse_ids) > 1: + raise ValidationError( + _('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 len(zero_rule) > 1: + raise ValidationError( + _('You can only have one zero confirmation rule per ' + 'warehouse.') + ) @api.onchange('rule_type') - def _get_rule_description(self): + 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 ' @@ -66,17 +68,19 @@ class StockCycleCountRule(models.Model): else: self.rule_description = _('(No description provided.)') + @api.multi @api.constrains('periodic_qty_per_period', 'periodic_count_period') def _check_negative_periodic(self): - if self.periodic_qty_per_period < 1: - raise UserError( - _('You cannot define a negative or null number of counts per ' - 'period.') - ) - if self.periodic_count_period < 0: - raise UserError( - _('You cannot define a negative period.') - ) + 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.') + ) + if rec.periodic_count_period < 0: + raise ValidationError( + _('You cannot define a negative period.') + ) @api.onchange('location_ids') def _get_warehouses(self): @@ -87,36 +91,45 @@ class StockCycleCountRule(models.Model): wh_ids = list(set(wh_ids)) self.warehouse_ids = self.env['stock.warehouse'].browse(wh_ids) - name = fields.Char('Name', required=True) - rule_type = fields.Selection(selection="_selection_rule_types", - string='Type of rule', - required=True) - rule_description = fields.Char(string='Rule Description', - compute='_get_rule_description') - active = fields.Boolean(string='Active', default=True) - periodic_qty_per_period = fields.Integer(string='Counts per period', - default=1) + name = fields.Char(required=True) + rule_type = fields.Selection( + selection="_selection_rule_types", + string='Type of rule', required=True, + ) + rule_description = fields.Char( + 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') turnover_inventory_value_threshold = fields.Float( - string='Turnover Inventory Value Threshold') - currency_id = fields.Many2one(comodel_name='res.currency', - string='Currency', - compute='_compute_currency') - accuracy_threshold = fields.Float(string='Minimum Accuracy Threshold', - digits=(3, 2)) + string='Turnover Inventory Value Threshold', + ) + currency_id = fields.Many2one( + comodel_name='res.currency', string='Currency', + compute='_compute_currency_id', + ) + accuracy_threshold = fields.Float( + 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') + 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') + 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') + column2='location_id', string='Zones where applied', + ) def compute_rule(self, locs): if self.rule_type == 'periodic': @@ -211,8 +224,9 @@ class StockCycleCountRule(models.Model): cycle_counts.append(cycle_count) return cycle_counts - @api.model + @api.multi def _compute_rule_accuracy(self, locs): + self.ensure_one() cycle_counts = [] for loc in locs: if loc.loc_accuracy < self.accuracy_threshold: diff --git a/stock_cycle_count/models/stock_location.py b/stock_cycle_count/models/stock_location.py index 23130e0da..3203ad574 100644 --- a/stock_cycle_count/models/stock_location.py +++ b/stock_cycle_count/models/stock_location.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# Copyright 2017-18 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). @@ -20,59 +20,67 @@ except (ImportError, IOError) as err: class StockLocation(models.Model): _inherit = 'stock.location' - @api.one + @api.multi def _compute_loc_accuracy(self): - history = self.env['stock.inventory'].search([ - ('location_id', '=', self.id), ('state', '=', 'done')]) - history = history.sorted(key=lambda r: r.write_date, reverse=True) - if history: - wh = self.get_warehouse() - if len(history) > wh.counts_for_accuracy_qty: - self.loc_accuracy = mean(history[:wh.counts_for_accuracy_qty]. - mapped('inventory_accuracy')) - else: - self.loc_accuracy = mean(history.mapped('inventory_accuracy')) + for rec in self: + 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 len(history) > wh.counts_for_accuracy_qty: + rec.loc_accuracy = mean( + history[:wh.counts_for_accuracy_qty].mapped( + 'inventory_accuracy')) + else: + rec.loc_accuracy = mean( + history.mapped('inventory_accuracy')) zero_confirmation_disabled = fields.Boolean( string='Disable Zero Confirmations', - default=False, help='Define whether this location will trigger a zero-confirmation ' 'validation when a rule for its warehouse is defined to perform ' - 'zero-confirmations.') + 'zero-confirmations.', + ) cycle_count_disabled = fields.Boolean( string='Exclude from Cycle Count', - default=False, - help='Define whether the location is going to be cycle counted.') - qty_variance_inventory_threshold = fields.Float('Acceptable Inventory ' - 'Quantity Variance ' - 'Threshold') + help='Define whether the location is going to be cycle counted.', + ) + qty_variance_inventory_threshold = fields.Float( + string='Acceptable Inventory Quantity Variance Threshold', + ) loc_accuracy = fields.Float( string='Inventory Accuracy', compute='_compute_loc_accuracy', - digits=(3, 2)) + digits=(3, 2), + ) - @api.model + @api.multi def _get_zero_confirmation_domain(self): + self.ensure_one() domain = [('location_id', '=', self.id)] return domain - @api.one + @api.multi def check_zero_confirmation(self): - if not self.zero_confirmation_disabled: - wh = self.get_warehouse() - 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( - self._get_zero_confirmation_domain()) - if not quants: - self.create_zero_confirmation_cycle_count() + 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)]) + if zero_rule: + quants = self.env['stock.quant'].search( + rec._get_zero_confirmation_domain()) + if not quants: + rec.create_zero_confirmation_cycle_count() + @api.multi def create_zero_confirmation_cycle_count(self): + 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()[0].strftime( + 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'), diff --git a/stock_cycle_count/models/stock_warehouse.py b/stock_cycle_count/models/stock_warehouse.py index cd2942b81..884d7c261 100644 --- a/stock_cycle_count/models/stock_warehouse.py +++ b/stock_cycle_count/models/stock_warehouse.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# Copyright 2017-18 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). @@ -30,8 +30,9 @@ class StockWarehouse(models.Model): help='Number of latest inventories used to calculate location ' 'accuracy') - @api.one + @api.multi def get_horizon_date(self): + self.ensure_one() date = datetime.today() delta = timedelta(self.cycle_count_planning_horizon) date_horizon = date + delta @@ -58,60 +59,64 @@ class StockWarehouse(models.Model): self._get_cycle_count_locations_search_domain(loc)) return locations - @api.model + @api.multi def _cycle_count_rules_to_compute(self): - rules = self.cycle_count_rule_ids.search([ - ('rule_type', '!=', 'zero'), ('warehouse_ids', '=', self.id)]) + self.ensure_one() + rules = self.env['stock.cycle.count.rule'].search([ + ('rule_type', '!=', 'zero'), ('warehouse_ids', 'in', self.ids)]) return rules - @api.one + @api.multi def action_compute_cycle_count_rules(self): - ''' Apply the rule in all the sublocations of a given warehouse(s) and + """ Apply the rule in all the sublocations of a given warehouse(s) and returns a list with required dates for the cycle count of each - location ''' - proposed_cycle_counts = [] - rules = self._cycle_count_rules_to_compute() - for rule in rules: - locations = self._search_cycle_count_locations(rule) - 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])) - for loc in locations: - proposed_for_loc = filter(lambda x: x['location'] == loc, - proposed_cycle_counts) - earliest_date = min([d['date'] for d in proposed_for_loc]) - cycle_count_proposed = 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] - if cycle_count_proposed['date'] < existing_earliest_date: - cc_to_update = existing_cycle_counts.search([ - ('date_deadline', '=', existing_earliest_date)]) - cc_to_update.write({ + location """ + for rec in self: + proposed_cycle_counts = [] + rules = rec._cycle_count_rules_to_compute() + for rule in rules: + locations = rec._search_cycle_count_locations(rule) + 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])) + for loc in locations: + proposed_for_loc = filter(lambda x: x['location'] == loc, + proposed_cycle_counts) + earliest_date = min([d['date'] for d in proposed_for_loc]) + cycle_count_proposed = 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] + 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 = datetime.strptime( + cycle_count_proposed['date'], + DEFAULT_SERVER_DATETIME_FORMAT) - datetime.today() + if not existing_cycle_counts and \ + delta.days < rec.cycle_count_planning_horizon: + self.env['stock.cycle.count'].create({ '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' }) - delta = datetime.strptime( - cycle_count_proposed['date'], - DEFAULT_SERVER_DATETIME_FORMAT) - datetime.today() - if not existing_cycle_counts and \ - delta.days < self.cycle_count_planning_horizon: - self.env['stock.cycle.count'].create({ - '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.model def cron_cycle_count(self): diff --git a/stock_cycle_count/tests/test_stock_cycle_count.py b/stock_cycle_count/tests/test_stock_cycle_count.py index 86bbbf64b..2d55fc488 100644 --- a/stock_cycle_count/tests/test_stock_cycle_count.py +++ b/stock_cycle_count/tests/test_stock_cycle_count.py @@ -233,7 +233,7 @@ class TestStockCycleCount(common.TransactionCase): self.rule_accuracy, self.zero_rule] for r in rules: - r._get_rule_description() + r._compute_rule_description() self.assertTrue(r.rule_description, 'No description provided') self.rule_accuracy._get_warehouses() self.assertEqual(self.rule_accuracy.warehouse_ids.ids, self.big_wh.ids,