mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[ADD] stock_cycle_count
This commit is contained in:
committed by
ArnauCForgeFlow
parent
87b598688f
commit
28675d2071
93
stock_cycle_count/README.rst
Normal file
93
stock_cycle_count/README.rst
Normal file
@@ -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
|
||||
<https://github.com/OCA/{project_repo}/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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Lois Rilo <lois.rilo@eficent.com>
|
||||
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
|
||||
|
||||
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.
|
||||
6
stock_cycle_count/__init__.py
Normal file
6
stock_cycle_count/__init__.py
Normal file
@@ -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
|
||||
33
stock_cycle_count/__openerp__.py
Normal file
33
stock_cycle_count/__openerp__.py
Normal file
@@ -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,
|
||||
}
|
||||
20
stock_cycle_count/data/cycle_count_ir_cron.xml
Normal file
20
stock_cycle_count/data/cycle_count_ir_cron.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record forcecreate="True"
|
||||
id="ir_cron_compute_cycle_count_action" model="ir.cron">
|
||||
<field name="name">Cycle Count Planner Computation</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model" eval="'stock.warehouse'"/>
|
||||
<field name="function" eval="'cron_cycle_count'"/>
|
||||
<field name="args" eval="'()'" />
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
17
stock_cycle_count/data/cycle_count_sequence.xml
Normal file
17
stock_cycle_count/data/cycle_count_sequence.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="seq_cycle_count" model="ir.sequence">
|
||||
<field name="name">Cycle Count</field>
|
||||
<field name="code">stock.cycle.count</field>
|
||||
<field name="prefix">CC/%(range_year)s/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
11
stock_cycle_count/models/__init__.py
Normal file
11
stock_cycle_count/models/__init__.py
Normal file
@@ -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
|
||||
88
stock_cycle_count/models/stock_cycle_count.py
Normal file
88
stock_cycle_count/models/stock_cycle_count.py
Normal file
@@ -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
|
||||
199
stock_cycle_count/models/stock_cycle_count_rule.py
Normal file
199
stock_cycle_count/models/stock_cycle_count_rule.py
Normal file
@@ -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
|
||||
34
stock_cycle_count/models/stock_inventory.py
Normal file
34
stock_cycle_count/models/stock_inventory.py
Normal file
@@ -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()
|
||||
91
stock_cycle_count/models/stock_location.py
Normal file
91
stock_cycle_count/models/stock_location.py
Normal file
@@ -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
|
||||
16
stock_cycle_count/models/stock_move.py
Normal file
16
stock_cycle_count/models/stock_move.py
Normal file
@@ -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
|
||||
110
stock_cycle_count/models/stock_warehouse.py
Normal file
110
stock_cycle_count/models/stock_warehouse.py
Normal file
@@ -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
|
||||
5
stock_cycle_count/security/ir.model.access.csv
Normal file
5
stock_cycle_count/security/ir.model.access.csv
Normal file
@@ -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
|
||||
|
BIN
stock_cycle_count/static/description/icon.png
Normal file
BIN
stock_cycle_count/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
73
stock_cycle_count/views/stock_cycle_count_rule_view.xml
Normal file
73
stock_cycle_count/views/stock_cycle_count_rule_view.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
<!-- Stock Cycle Count Rule List view -->
|
||||
<record id="stock_cycle_count_rule_tree_view" model="ir.ui.view">
|
||||
<field name="name">stock.cycle.count.rule.tree</field>
|
||||
<field name="model">stock.cycle.count.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Stock Cycle Count">
|
||||
<field name="name"/>
|
||||
<field name="warehouse_ids"/>
|
||||
<field name="rule_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Stock Cycle Count Rule Form view -->
|
||||
<record id="stock_cycle_count_rule_form_view" model="ir.ui.view">
|
||||
<field name="name">stock.cycle.count.rule.form</field>
|
||||
<field name="model">stock.cycle.count.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="cycle counts test:">
|
||||
|
||||
<sheet>
|
||||
<group name="top">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="rule_type"/>
|
||||
<field name="rule_description"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group name="specific rule fields">
|
||||
<field name="periodic_qty_per_period"
|
||||
attrs="{'invisible': [('rule_type', '!=', 'periodic')]}"/>
|
||||
<field name="periodic_count_period"
|
||||
attrs="{'invisible': [('rule_type', '!=', 'periodic')]}"/>
|
||||
<field name="turnover_inventory_value_threshold"
|
||||
attrs="{'invisible': [('rule_type', '!=', 'turnover')]}"
|
||||
widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="currency_id"
|
||||
invisible="True"/>
|
||||
<label for="accuracy_threshold"
|
||||
attrs="{'invisible': [('rule_type', '!=', 'accuracy')]}"/>
|
||||
<div attrs="{'invisible': [('rule_type', '!=', 'accuracy')]}">
|
||||
<field name="accuracy_threshold" class="oe_inline"/> %
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Applied in">
|
||||
<field name="warehouse_ids"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open Stock Cycle Count Rule -->
|
||||
<act_window id="action_stock_cycle_count_rules"
|
||||
name="Stock Cycle Count Rules"
|
||||
res_model="stock.cycle.count.rule"
|
||||
view_mode="tree,form" />
|
||||
|
||||
<menuitem id="menu_stock_cycle_count_rule"
|
||||
name="Cycle Count Rules"
|
||||
parent="stock.menu_stock_config_settings"
|
||||
action="action_stock_cycle_count_rules" />
|
||||
|
||||
</odoo>
|
||||
88
stock_cycle_count/views/stock_cycle_count_view.xml
Normal file
88
stock_cycle_count/views/stock_cycle_count_view.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
<!-- Stock Cycle Count List view -->
|
||||
<record id="stock_cycle_count_tree_view" model="ir.ui.view">
|
||||
<field name="name">stock.cycle.count.tree</field>
|
||||
<field name="model">stock.cycle.count</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Stock Cycle Count">
|
||||
<field name="name"/>
|
||||
<field name="location_id"/>
|
||||
<field name="cycle_count_rule_id"/>
|
||||
<field name="responsible_id"/>
|
||||
<field name="date_deadline"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_cycle_count_form_view" model="ir.ui.view">
|
||||
<field name="name">stock.cycle.count.form</field>
|
||||
<field name="model">stock.cycle.count</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="cycle counts test:">
|
||||
<header>
|
||||
<button name="action_create_inventory_adjustment"
|
||||
type="object" states="draft"
|
||||
string="Confirm" class="oe_highlight" />
|
||||
<button name="do_cancel" type="object"
|
||||
string="Cancel" states="draft"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,open,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box"
|
||||
attrs="{'invisible':
|
||||
[('state', 'not in', ('draft', 'open', 'done'))]}">
|
||||
<button name="action_view_inventory"
|
||||
type="object" class="oe_stat_button"
|
||||
icon="fa-building-o">
|
||||
<field name="inventory_adj_count"
|
||||
widget="statinfo"
|
||||
help="Inventory adjustments associated"
|
||||
modifiers="{'readonly': true}"
|
||||
string="Inventory Adjustments"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label string="Cycle Count"/>
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group name="top">
|
||||
<group name="left">
|
||||
<field name="location_id">Location</field>
|
||||
<field name="cycle_count_rule_id"/>
|
||||
</group>
|
||||
<group name="right">
|
||||
<field name="responsible_id">Assigned to</field>
|
||||
<field name="date_deadline">Deadline Date</field>
|
||||
<field name="company_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open Stock Cycle Count -->
|
||||
<act_window id="action_stock_cycle_count"
|
||||
name="Stock Cycle Count"
|
||||
res_model="stock.cycle.count"
|
||||
view_mode="tree,form" />
|
||||
<menuitem id="menu_stock_cycle_count"
|
||||
name="Cycle Counts" parent="stock.menu_stock_inventory_control"
|
||||
action="action_stock_cycle_count" />
|
||||
|
||||
</odoo>
|
||||
34
stock_cycle_count/views/stock_inventory_view.xml
Normal file
34
stock_cycle_count/views/stock_inventory_view.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_inventory_tree" model="ir.ui.view">
|
||||
<field name="name">Inventory tree view - cycle count extension</field>
|
||||
<field name="model">stock.inventory</field>
|
||||
<field name="inherit_id" ref="stock.view_inventory_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="after">
|
||||
<field name="cycle_count_id"/>
|
||||
<field name="inventory_accuracy"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_inventory_form" model="ir.ui.view">
|
||||
<field name="name">Inventory form view - cycle count extension </field>
|
||||
<field name="model">stock.inventory</field>
|
||||
<field name="inherit_id" ref="stock.view_inventory_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="after">
|
||||
<field name="cycle_count_id"/>
|
||||
<label for="inventory_accuracy"/>
|
||||
<div>
|
||||
<field name="inventory_accuracy" class="oe_inline"/> %
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
37
stock_cycle_count/views/stock_location_view.xml
Normal file
37
stock_cycle_count/views/stock_location_view.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record model="ir.actions.act_window" id="act_accuracy_stats">
|
||||
<field name="domain">[('location_id', '=', active_ids),
|
||||
('state', '=', 'done')]</field>
|
||||
<field name="name">Accuracy Stats</field>
|
||||
<field name="res_model">stock.inventory</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_location_form" model="ir.ui.view">
|
||||
<field name="name">Location form - cycle count extension</field>
|
||||
<field name="model">stock.location</field>
|
||||
<field name="inherit_id" ref="stock.view_location_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button string="Accuracy Stats" class="oe_stat_button"
|
||||
icon="fa-line-chart" name="%(act_accuracy_stats)d"
|
||||
type="action"/>
|
||||
</xpath>
|
||||
<field name="active" position="after">
|
||||
<field name="zero_confirmation_disabled"/>
|
||||
<field name="cycle_count_disabled"/>
|
||||
<label for="loc_accuracy"/>
|
||||
<div>
|
||||
<field name="loc_accuracy" class="oe_inline"/> %
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
48
stock_cycle_count/views/stock_warehouse_view.xml
Normal file
48
stock_cycle_count/views/stock_warehouse_view.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_warehouse_form" model="ir.ui.view">
|
||||
<field name="name">Warehouse form - cycle count extension</field>
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse"/>
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="before">
|
||||
<group string="Cycle Counting">
|
||||
<group >
|
||||
<field name="cycle_count_planning_horizon"/>
|
||||
<field name="counts_for_accuracy_qty"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="cycle_count_rule_ids" nolabel="1">Cycle
|
||||
count rules</field>
|
||||
</group>
|
||||
</group>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_server_warehouse_execute_cycle_count"
|
||||
model="ir.actions.server">
|
||||
<field name="name">Compute Cycle Count Rules</field>
|
||||
<field name="condition">True</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_stock_warehouse" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">self.action_compute_cycle_count_rules(cr, uid, context.get('active_ids', []), context=context)</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.values" id="action_warehouse_execute_cycle_count">
|
||||
<field name="name">action_warehouse_execute_cycle_count</field>
|
||||
<field name="action_id"
|
||||
ref="action_server_warehouse_execute_cycle_count" />
|
||||
<field name="value" eval="'ir.actions.server,' + str(ref('action_server_warehouse_execute_cycle_count'))" />
|
||||
<field name="key">action</field>
|
||||
<field name="model_id" ref="model_stock_warehouse" />
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="key2">client_action_multi</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user