diff --git a/stock_cycle_count/README.rst b/stock_cycle_count/README.rst new file mode 100644 index 000000000..b2a9cecb9 --- /dev/null +++ b/stock_cycle_count/README.rst @@ -0,0 +1,93 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================= +Stock Cycle Count +================= + +This module adds the capability to execute a cycle count strategy in a +warehouse through different rules defined by the user. + + +Installation +============ + +To install this module, you need to: + +* Download this module to your addons path. +* Install the module in your database. + +Configuration +============= + +You can configure the rules to compute the cycle count, acting as follow: + +#. Go to "Inventory > Configuration > Cycle Count Rules" +#. Create as much cycle count rules as you want. +#. Assign the rules to the Warehouse where you want to apply the rules in. +#. Set a "Cycle Count Planning Horizon" for each warehouse. + +.. figure:: path/to/local/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +Once you have some rules configured for your warehouses, you can proceed as +is described below. + +#. Go to "Inventory > Configuration > Warehouse Management > Warehouses". +#. Select all the warehouses you want to compute the rules in. +#. Click on "Action" and then in "Compute Cycle Count Rules". +#. Go to "Inventory Control > Cycle Counts". +#. Select a Cycle Count planned an confirm it, this will create a draft + Inventory Adjustment. +#. In the right top corner of the form view you can access the generated + Inventory Adjustment. +#. Proceed with the Inventory Adjustment as usual. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/153/9.0 + +.. repo_id is available in https://github.com/OCA/stock-logistics-warehouse +.. branch is "9.0" for example + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Lois Rilo +* Jordi Ballester Alomar + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/stock_cycle_count/__init__.py b/stock_cycle_count/__init__.py new file mode 100644 index 000000000..08f93b3a4 --- /dev/null +++ b/stock_cycle_count/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 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 . import models diff --git a/stock_cycle_count/__openerp__.py b/stock_cycle_count/__openerp__.py new file mode 100644 index 000000000..e903912fb --- /dev/null +++ b/stock_cycle_count/__openerp__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# 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). +{ + "name": "Stock Cycle Count", + "summary": "Adds the capability to schedule cycle counts in a " + "warehouse through different rules defined by the user", + "version": "9.0.1.0.0", + "author": "Eficent, " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Warehouse Management", + "depends": ["stock", + "mail", + "stock_inventory_discrepancy", + "stock_inventory_exclude_sublocation"], + "external_dependencies": { + "python": ['numpy'], + }, + "data": [ + 'views/stock_cycle_count_view.xml', + 'views/stock_cycle_count_rule_view.xml', + 'views/stock_warehouse_view.xml', + 'views/stock_inventory_view.xml', + 'views/stock_location_view.xml', + 'data/cycle_count_sequence.xml', + 'data/cycle_count_ir_cron.xml', + 'security/ir.model.access.csv'], + "license": "AGPL-3", + 'installable': True, + 'application': False, +} diff --git a/stock_cycle_count/data/cycle_count_ir_cron.xml b/stock_cycle_count/data/cycle_count_ir_cron.xml new file mode 100644 index 000000000..0f850b896 --- /dev/null +++ b/stock_cycle_count/data/cycle_count_ir_cron.xml @@ -0,0 +1,20 @@ + + + + + + + Cycle Count Planner Computation + + 1 + days + -1 + + + + + + + diff --git a/stock_cycle_count/data/cycle_count_sequence.xml b/stock_cycle_count/data/cycle_count_sequence.xml new file mode 100644 index 000000000..ca0b5dd0c --- /dev/null +++ b/stock_cycle_count/data/cycle_count_sequence.xml @@ -0,0 +1,17 @@ + + + + + + + + Cycle Count + stock.cycle.count + CC/%(range_year)s/ + 5 + + + + + diff --git a/stock_cycle_count/models/__init__.py b/stock_cycle_count/models/__init__.py new file mode 100644 index 000000000..0ef49bf90 --- /dev/null +++ b/stock_cycle_count/models/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# 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 . import stock_cycle_count +from . import stock_cycle_count_rule +from . import stock_location +from . import stock_inventory +from . import stock_warehouse +from . import stock_move diff --git a/stock_cycle_count/models/stock_cycle_count.py b/stock_cycle_count/models/stock_cycle_count.py new file mode 100644 index 000000000..a5cac5aa3 --- /dev/null +++ b/stock_cycle_count/models/stock_cycle_count.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# 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 openerp import api, fields, models + + +class StockCycleCount(models.Model): + _name = 'stock.cycle.count' + _inherit = 'mail.thread' + + @api.one + def _count_inventory_adj(self): + self.inventory_adj_count = len(self.stock_adjustment_ids) + + @api.model + def create(self, vals): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'stock.cycle.count') or '' + return super(StockCycleCount, self).create(vals) + + @api.model + def _company_get(self): + company_id = self.env['res.company']._company_default_get(self._name) + return company_id + + name = fields.Char(string='Name', readonly=True) + location_id = fields.Many2one(comodel_name='stock.location', + string='Location', + required=True) + responsible_id = fields.Many2one(comodel_name='res.users', + string='Assigned to') + date_deadline = fields.Date(string='Required Date') + cycle_count_rule_id = fields.Many2one( + comodel_name='stock.cycle.count.rule', + string='Cycle count rule', + required=True) + state = fields.Selection(selection=[ + ('draft', 'Planned'), + ('open', 'Execution'), + ('cancelled', 'Cancelled'), + ('done', 'Done') + ], string='State', default='draft') + stock_adjustment_ids = fields.One2many(comodel_name='stock.inventory', + inverse_name='cycle_count_id', + string='Inventory Adjustment') + inventory_adj_count = fields.Integer(compute=_count_inventory_adj) + company_id = fields.Many2one(comodel_name='res.company', + string='Company', + required=True, + default=_company_get) + + @api.one + def do_cancel(self): + self.state = 'cancelled' + + @api.model + def _prepare_inventory_adjustment(self): + return { + 'name': 'INV/{}'.format(self.name), + 'cycle_count_id': self.id, + 'location_id': self.location_id.id, + 'exclude_sublocation': True + } + + @api.one + def action_create_inventory_adjustment(self): + data = self._prepare_inventory_adjustment() + self.env['stock.inventory'].create(data) + self.state = 'open' + return True + + @api.multi + def action_view_inventory(self): + action = self.env.ref('stock.action_inventory_form') + result = action.read()[0] + result['context'] = {} + adjustment_ids = sum([cycle_count.stock_adjustment_ids.ids + for cycle_count in self], []) + if len(adjustment_ids) > 1: + result['domain'] = \ + "[('id','in',[" + ','.join(map(str, 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 + return result diff --git a/stock_cycle_count/models/stock_cycle_count_rule.py b/stock_cycle_count/models/stock_cycle_count_rule.py new file mode 100644 index 000000000..23008e1f2 --- /dev/null +++ b/stock_cycle_count/models/stock_cycle_count_rule.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# 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 openerp import api, fields, models, _ +from openerp.exceptions import UserError +from datetime import timedelta, datetime + + +class StockCycleCountRule(models.Model): + _name = 'stock.cycle.count.rule' + + @api.one + def _compute_currency(self): + self.currency_id = self.env.user.company_id.currency_id + + @api.model + def _selection_rule_types(self): + return [ + ('periodic', _('Periodic')), + ('turnover', _('Value Turnover')), + ('accuracy', _('Minimum Accuracy')), + ('zero', _('Zero Confirmation'))] + + @api.one + @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.') + ) + + @api.onchange('rule_type') + def _get_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.') + else: + self.rule_description = _('(No description provided.)') + + @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.') + ) + + 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) + 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)) + warehouse_ids = fields.Many2many(comodel_name='stock.warehouse', + relation='warehouse_cycle_count_rule_rel', + column1='rule_id', + column2='warehouse_id', + string='Applied in') + + def compute_rule(self, locs): + if self.rule_type == 'periodic': + proposed_cycle_counts = self._compute_rule_periodic(locs) + elif self.rule_type == 'turnover': + proposed_cycle_counts = self._compute_rule_turnover(locs) + 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': date.strftime('%Y-%m-%d %H:%M:%S'), + 'location': location, + 'rule_type': self + } + return cycle_count + + @api.model + def _compute_rule_periodic(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') + if last_inventories: + latest_inventory = sorted(last_inventories, reverse=True)[0] + try: + period = self.periodic_count_period / \ + self.periodic_qty_per_period + next_date = datetime.strptime( + latest_inventory, '%Y-%m-%d %H:%M:%S') + timedelta( + days=period) + except Exception as e: + raise UserError( + _('Error found determining the frequency of periodic ' + 'cycle count rule. %s') % e.message) + else: + next_date = datetime.today() + cycle_count = self._propose_cycle_count(next_date, loc) + cycle_counts.append(cycle_count) + return cycle_counts + + @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')]) + return moves + + @api.model + def _compute_turnover(self, move): + turnover = move.product_uom_qty * move.product_id.standard_price + return turnover + + @api.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') + if last_inventories: + latest_inventory = sorted(last_inventories, reverse=True)[0] + moves = self._get_turnover_moves(loc, latest_inventory) + if moves: + total_turnover = 0.0 + for m in moves: + turnover = self._compute_turnover(m) + total_turnover += turnover + try: + if total_turnover > \ + self.turnover_inventory_value_threshold: + next_date = datetime.today() + 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') % e.message) + else: + next_date = datetime.today() + cycle_count = self._propose_cycle_count(next_date, loc) + cycle_counts.append(cycle_count) + return cycle_counts + + @api.model + def _compute_rule_accuracy(self, locs): + cycle_counts = [] + for loc in locs: + if loc.loc_accuracy < self.accuracy_threshold: + next_date = datetime.today() + cycle_count = self._propose_cycle_count(next_date, loc) + cycle_counts.append(cycle_count) + return cycle_counts diff --git a/stock_cycle_count/models/stock_inventory.py b/stock_cycle_count/models/stock_inventory.py new file mode 100644 index 000000000..d01e09095 --- /dev/null +++ b/stock_cycle_count/models/stock_inventory.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# 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 openerp import api, fields, models + + +class StockInventory(models.Model): + _inherit = 'stock.inventory' + + @api.one + def _compute_inventory_accuracy(self): + total_qty = sum(self.line_ids.mapped('theoretical_qty')) + abs_discrepancy = sum(self.line_ids.mapped( + lambda x: abs(x.discrepancy_qty))) + if total_qty: + self.inventory_accuracy = 100 * (total_qty - abs_discrepancy) / \ + total_qty + if not self.line_ids and self.state == 'done': + self.inventory_accuracy = 100.0 + + cycle_count_id = fields.Many2one(comodel_name='stock.cycle.count', + string='Stock Cycle Count', + ondelete='cascade') + inventory_accuracy = fields.Float(string='Accuracy', + compute=_compute_inventory_accuracy, + digits=(3, 2)) + + @api.multi + def action_done(self): + if self.cycle_count_id: + self.cycle_count_id.state = 'done' + return super(StockInventory, self).action_done() diff --git a/stock_cycle_count/models/stock_location.py b/stock_cycle_count/models/stock_location.py new file mode 100644 index 000000000..b429bd90b --- /dev/null +++ b/stock_cycle_count/models/stock_location.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# 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). + +import logging + +from openerp import api, fields, models, tools +from datetime import datetime +_logger = logging.getLogger(__name__) + +try: + from numpy import mean + NUMPY_PATH = tools.find_in_path('numpy') +except (ImportError, IOError) as err: + _logger.debug(err) + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + @api.one + 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_id = self.get_warehouse(self) + wh = self.env['stock.warehouse'].browse(wh_id) + 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')) + + 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.') + 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') + loc_accuracy = fields.Float(string='Inventory Accuracy', + compute=_compute_loc_accuracy, + digits=(3, 2)) + + @api.model + def _get_zero_confirmation_domain(self): + domain = [('location_id', '=', self.id)] + return domain + + @api.one + def check_zero_confirmation(self): + if not self.zero_confirmation_disabled: + wh_id = self.get_warehouse(self) + wh = self.env['stock.warehouse'].browse(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( + self._get_zero_confirmation_domain()) + if not quants: + self.create_zero_confirmation_cycle_count() + + def create_zero_confirmation_cycle_count(self): + date = datetime.today().strftime('%Y-%m-%d %H:%M:%S') + wh_id = self.get_warehouse(self) + date_horizon = self.env['stock.warehouse'].browse( + wh_id).get_horizon_date()[0].strftime('%Y-%m-%d %H:%M:%S') + 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' + }) + return True diff --git a/stock_cycle_count/models/stock_move.py b/stock_cycle_count/models/stock_move.py new file mode 100644 index 000000000..192973ff2 --- /dev/null +++ b/stock_cycle_count/models/stock_move.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# 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 openerp import api, models + + +class StockMove(models.Model): + _inherit = 'stock.move' + + @api.one + def action_done(self): + super(StockMove, self).action_done() + self.location_id.check_zero_confirmation() + return True diff --git a/stock_cycle_count/models/stock_warehouse.py b/stock_cycle_count/models/stock_warehouse.py new file mode 100644 index 000000000..89d36eae0 --- /dev/null +++ b/stock_cycle_count/models/stock_warehouse.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# 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 openerp import api, fields, models +from datetime import datetime, timedelta + + +class StockWarehouse(models.Model): + _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') + 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.') + counts_for_accuracy_qty = fields.Integer( + string='Inventories for location accuracy calculation', + default=1, + help='Number of latest inventories used to calculate location ' + 'accuracy') + + @api.one + def get_horizon_date(self): + date = datetime.today() + delta = timedelta(self.cycle_count_planning_horizon) + date_horizon = date + delta + return date_horizon + + @api.model + def _get_cycle_count_locations_search_domain(self): + wh_parent_left = self.view_location_id.parent_left + wh_parent_right = self.view_location_id.parent_right + domain = [('parent_left', '>', wh_parent_left), + ('parent_right', '<', wh_parent_right), + ('cycle_count_disabled', '=', False)] + return domain + + @api.model + def _search_cycle_count_locations(self): + locations = self.env['stock.location'].search( + self._get_cycle_count_locations_search_domain()) + return locations + + @api.model + def _cycle_count_rules_to_compute(self): + rules = self.cycle_count_rule_ids.search([ + ('rule_type', '!=', 'zero')]) + return rules + + @api.one + def action_compute_cycle_count_rules(self): + ''' 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 = [] + locations = self._search_cycle_count_locations() + rules = self._cycle_count_rules_to_compute() + if locations: + for rule in rules: + 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 and cycle_count_proposed['date'] <\ + existing_cycle_counts.date_deadline: + 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' + }) + existing_cycle_counts.state = 'cancelled' + delta = datetime.strptime( + cycle_count_proposed['date'], + '%Y-%m-%d %H:%M:%S') - 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): + whs = self.search([]) + whs.action_compute_cycle_count_rules() + return True diff --git a/stock_cycle_count/security/ir.model.access.csv b/stock_cycle_count/security/ir.model.access.csv new file mode 100644 index 000000000..30d2c432e --- /dev/null +++ b/stock_cycle_count/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_cycle_count_user,stock.cycle.count user,model_stock_cycle_count,stock.group_stock_user,1,1,1,0 +access_stock_cycle_count_manager,stock.cycle.count manager,model_stock_cycle_count,stock.group_stock_manager,1,1,1,1 +access_stock_cycle_count_rule_user,stock.cycle.count.rule user,model_stock_cycle_count_rule,stock.group_stock_user,1,0,0,0 +access_stock_cycle_count_rule_manager,stock.cycle.count.rule manager,model_stock_cycle_count_rule,stock.group_stock_manager,1,1,1,1 diff --git a/stock_cycle_count/static/description/icon.png b/stock_cycle_count/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_cycle_count/static/description/icon.png differ diff --git a/stock_cycle_count/views/stock_cycle_count_rule_view.xml b/stock_cycle_count/views/stock_cycle_count_rule_view.xml new file mode 100644 index 000000000..94e9389de --- /dev/null +++ b/stock_cycle_count/views/stock_cycle_count_rule_view.xml @@ -0,0 +1,73 @@ + + + + + + + stock.cycle.count.rule.tree + stock.cycle.count.rule + + + + + + + + + + + stock.cycle.count.rule.form + stock.cycle.count.rule + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + +
diff --git a/stock_cycle_count/views/stock_cycle_count_view.xml b/stock_cycle_count/views/stock_cycle_count_view.xml new file mode 100644 index 000000000..3349c2700 --- /dev/null +++ b/stock_cycle_count/views/stock_cycle_count_view.xml @@ -0,0 +1,88 @@ + + + + + + + stock.cycle.count.tree + stock.cycle.count + + + + + + + + + + + + + + stock.cycle.count.form + stock.cycle.count + +
+
+
+ +
+ +
+
+
+ + + + Location + + + + Assigned to + Deadline Date + + + + +
+
+ + +
+ +
+
+
+ + + + + +
diff --git a/stock_cycle_count/views/stock_inventory_view.xml b/stock_cycle_count/views/stock_inventory_view.xml new file mode 100644 index 000000000..6bdf9c266 --- /dev/null +++ b/stock_cycle_count/views/stock_inventory_view.xml @@ -0,0 +1,34 @@ + + + + + + + Inventory tree view - cycle count extension + stock.inventory + + + + + + + + + + + Inventory form view - cycle count extension + stock.inventory + + + + + + + + + diff --git a/stock_cycle_count/views/stock_location_view.xml b/stock_cycle_count/views/stock_location_view.xml new file mode 100644 index 000000000..180adff21 --- /dev/null +++ b/stock_cycle_count/views/stock_location_view.xml @@ -0,0 +1,37 @@ + + + + + + + [('location_id', '=', active_ids), + ('state', '=', 'done')] + Accuracy Stats + stock.inventory + form + tree,form + + + + Location form - cycle count extension + stock.location + + + +