Add count wizard on inventory

issue7515
review49461002
This commit is contained in:
Cédric Krier committed 2018-07-27 17:07:31 +02:00
1 parent 973dd92330
commit e5852ab668
10 files changed
+344 -14

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Add count wizard on inventory
* Remove support for Python 2.7
* Prevent delete lines if inventory is not draft or canceled
* Manage same input and storage location in customer return shipment
+6 -3
View File
@@ -7,7 +7,7 @@ from .shipment import *
from .period import *
from .move import *
from .product import *
from .inventory import *
from . import inventory
from .configuration import *
from . import party
@@ -36,8 +36,10 @@ def register():
ProductByLocationContext,
ProductQuantitiesByWarehouse,
ProductQuantitiesByWarehouseContext,
Inventory,
InventoryLine,
inventory.Inventory,
inventory.InventoryLine,
inventory.CountSearch,
inventory.CountQuantity,
Configuration,
ConfigurationSequence,
ConfigurationLocation,
@@ -47,6 +49,7 @@ def register():
AssignShipmentInternal,
AssignShipmentInReturn,
RecomputeCostPrice,
inventory.Count,
party.PartyReplace,
party.PartyErase,
module='stock', type_='wizard')
+8 -5
View File
@@ -261,11 +261,14 @@ Inventory
Inventories allow to control and update stock levels. They are mainly
composed of two locations (a Storage location and a Lost And Found
location), and a list of inventory lines. A button allow to
auto-complete inventory lines with respect to the expected quantities
for each product in the location. Inventory lines consist of: a
product and it's default unit of measure, an expected quantity and the
real quantity (the real products on the shelves).
location), and a list of inventory lines.
Inventory lines consist of a product and it's default unit of measure, an
expected quantity and the real quantity (the real products on the shelves).
A button allows to auto-complete inventory lines with respect to the expected
quantities for each product in the location.
Another button allows to launch a wizard to count products by adding the
quantity to the existing matching line.
When the inventory is confirmed, moves are created to balance expected
quantities and real ones.
+153 -2
View File
@@ -3,11 +3,13 @@
from sql import Null
from trytond.model import Workflow, Model, ModelView, ModelSQL, fields, Check
from trytond.pyson import Eval, Bool
from trytond.pyson import Eval, Bool, If
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.wizard import Wizard, StateView, StateTransition, Button
__all__ = ['Inventory', 'InventoryLine']
__all__ = ['Inventory', 'InventoryLine',
'Count', 'CountSearch', 'CountQuantity']
STATES = {
'readonly': Eval('state') != 'draft',
@@ -86,6 +88,10 @@ class Inventory(Workflow, ModelSQL, ModelView):
'readonly': Eval('state') != 'draft',
'depends': ['state'],
},
'count': {
'readonly': Eval('state') != 'draft',
'depends': ['state'],
},
})
@classmethod
@@ -274,6 +280,11 @@ class Inventory(Workflow, ModelSQL, ModelView):
if to_write:
Line.write(*to_write)
@classmethod
@ModelView.button_action('stock.wizard_inventory_count')
def count(cls, inventories):
cls.complete_lines(inventories)
class InventoryLine(ModelSQL, ModelView):
'Stock Inventory Line'
@@ -476,3 +487,143 @@ class InventoryLine(ModelSQL, ModelView):
'line': line.rec_name,
})
super(InventoryLine, cls).delete(lines)
class Count(Wizard):
"Stock Inventory Count"
__name__ = 'stock.inventory.count'
start_state = 'search'
search = StateView(
'stock.inventory.count.search',
'stock.inventory_count_search_view_form', [
Button("End", 'end', 'tryton-cancel'),
Button("Select", 'quantity', 'tryton-go-next', default=True),
])
quantity = StateView(
'stock.inventory.count.quantity',
'stock.inventory_count_quantity_view_form', [
Button("Cancel", 'search', 'tryton-cancel'),
Button("Add", 'add', 'tryton-ok', default=True),
])
add = StateTransition()
@classmethod
def __setup__(cls):
super(Count, cls).__setup__()
cls._error_messages.update({
'create_line': "No existing line found for %(search)s.",
})
def default_quantity(self, fields):
pool = Pool()
Inventory = pool.get('stock.inventory')
InventoryLine = pool.get('stock.inventory.line')
context = Transaction().context
inventory = Inventory(context['active_id'])
values = {}
lines = InventoryLine.search(self.get_line_domain(inventory), limit=1)
if not lines:
warning_name = '%s.%s.count_create' % (
inventory, self.search.search)
self.raise_user_warning(warning_name, 'create_line', {
'search': self.search.search.rec_name,
})
line, = InventoryLine.create([self.get_line_values(inventory)])
else:
line, = lines
values['line'] = line.id
values['product'] = line.product.id
values['uom'] = line.uom.id
values['unit_digits'] = line.unit_digits
if line.uom.rounding == 1:
values['quantity_added'] = 1
return values
def get_line_domain(self, inventory):
pool = Pool()
Product = pool.get('product.product')
domain = [
('inventory', '=', inventory.id),
]
if isinstance(self.search.search, Product):
domain.append(('product', '=', self.search.search.id))
return domain
def get_line_values(self, inventory):
pool = Pool()
Product = pool.get('product.product')
InventoryLine = pool.get('stock.inventory.line')
values = InventoryLine.create_values4complete(inventory, 0)
if isinstance(self.search.search, Product):
values['product'] = self.search.search.id
return values
def transition_add(self):
if self.quantity.line and self.quantity.quantity_added:
line = self.quantity.line
if line.quantity:
line.quantity += self.quantity.quantity_added
else:
line.quantity = self.quantity.quantity_added
line.save()
return 'search'
class CountSearch(ModelView):
"Stock Inventory Count"
__name__ = 'stock.inventory.count.search'
search = fields.Reference(
"Search", [
('product.product', "Product"),
],
required=True,
domain=[If(Eval('search_model') == 'product.product',
[
('type', '=', 'goods'),
('consumable', '=', False),
],
[])],
depends=['search_model'])
search_model = fields.Function(fields.Selection(
'get_search_models', "Search Model"),
'on_change_with_search_model')
@classmethod
def default_search(cls):
return 'product.product,-1'
@classmethod
def get_search_models(cls):
return cls.fields_get(['search'])['search']['selection']
@fields.depends('search')
def on_change_with_search_model(self, name=None):
if self.search:
return self.search.__name__
class CountQuantity(ModelView):
"Stock Inventory Count"
__name__ = 'stock.inventory.count.quantity'
line = fields.Many2One(
'stock.inventory.line', "Line", readonly=True, required=True)
product = fields.Many2One('product.product', "Product", readonly=True)
uom = fields.Many2One('product.uom', "UOM", readonly=True)
quantity_resulting = fields.Float(
"Resulting Quantity", digits=(16, Eval('unit_digits', 2)),
readonly=True, depends=['unit_digits'])
quantity_added = fields.Float(
"Added Quantity", digits=(16, Eval('unit_digits', 2)), required=True,
depends=['unit_digits'])
unit_digits = fields.Integer("Unit Digits", readonly=True)
@fields.depends('quantity_added', 'line')
def on_change_quantity_added(self):
if self.line:
self.quantity_resulting = (
(self.line.quantity or 0) + (self.quantity_added or 0))
+26 -1
View File
@@ -116,11 +116,36 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.model.button" id="inventory_complete_lines_button">
<field name="name">complete_lines</field>
<field name="string">Complete Inventory</field>
<field name="string">Complete</field>
<field name="help">Add an inventory line for each missing products</field>
<field name="model" search="[('model', '=', 'stock.inventory')]"/>
</record>
<record model="ir.model.button" id="inventory_count_button">
<field name="name">count</field>
<field name="string">Count</field>
<field name="help">Launch the wizard to count products</field>
<field name="model" search="[('model', '=', 'stock.inventory')]"/>
</record>
<record model="ir.action.wizard" id="wizard_inventory_count">
<field name="name">Count</field>
<field name="wiz_name">stock.inventory.count</field>
<field name="model">stock.inventory</field>
</record>
<record model="ir.ui.view" id="inventory_count_search_view_form">
<field name="model">stock.inventory.count.search</field>
<field name="type">form</field>
<field name="name">inventory_count_search_form</field>
</record>
<record model="ir.ui.view" id="inventory_count_quantity_view_form">
<field name="model">stock.inventory.count.quantity</field>
<field name="type">form</field>
<field name="name">inventory_count_quantity_form</field>
</record>
<record model="ir.sequence.type" id="sequence_type_inventory">
<field name="name">Inventory</field>
<field name="code">stock.inventory</field>
+120
View File
@@ -0,0 +1,120 @@
==============================
Stock Inventory Count Scenario
==============================
Imports::
>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> today = datetime.date.today()
Install stock Module::
>>> config = activate_modules('stock')
Create company::
>>> _ = create_company()
>>> company = get_company()
Get stock locations::
>>> Location = Model.get('stock.location')
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
Create products::
>>> ProductUom = Model.get('product.uom')
>>> ProductTemplate = Model.get('product.template')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> template = ProductTemplate()
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('300')
>>> template.cost_price_method = 'average'
>>> product, = template.products
>>> product.cost_price = Decimal('80')
>>> template.save()
>>> product, = template.products
>>> kg, = ProductUom.find([('name', '=', 'Kilogram')])
>>> template2 = ProductTemplate()
>>> template2.name = 'Product'
>>> template2.default_uom = kg
>>> template2.type = 'goods'
>>> template2.list_price = Decimal('140')
>>> template2.cost_price_method = 'average'
>>> product2, = template2.products
>>> product2.cost_price = Decimal('60')
>>> template2.save()
>>> product2, = template2.products
Create an inventory::
>>> Inventory = Model.get('stock.inventory')
>>> inventory = Inventory()
>>> inventory.location = storage_loc
>>> inventory.empty_quantity = 'keep'
>>> inventory.save()
Count inventory::
>>> _ = inventory.click('count')
>>> count = Wizard('stock.inventory.count', [inventory])
>>> count.form.search = product
>>> count.execute('quantity') # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
UserWarning: ...
>>> Model.get('res.user.warning')(user=config.user,
... name='stock.inventory,%s.product.product,%s.count_create' % (
... inventory.id, product.id)).save()
>>> count.execute('quantity')
>>> count.form.quantity_added
1
>>> count.form.quantity_resulting
1
>>> count.execute('add')
>>> count.form.search = product
>>> count.execute('quantity')
>>> count.form.quantity_resulting
2.0
>>> count.execute('add')
>>> count.form.search = product2
>>> Model.get('res.user.warning')(user=config.user,
... name='stock.inventory,%s.product.product,%s.count_create' % (
... inventory.id, product2.id)).save()
>>> count.execute('quantity')
>>> count.form.quantity_added
>>> count.form.quantity_resulting
>>> count.form.quantity_added = 10
>>> count.form.quantity_resulting
10.0
>>> count.execute('add')
>>> count.execute('end')
Check inventory::
>>> len(inventory.lines)
2
>>> line1, = [l for l in inventory.lines if l.product == product]
>>> line1.quantity
2.0
>>> line2, = [l for l in inventory.lines if l.product == product2]
>>> line2.quantity
10.0
+5
View File
@@ -1296,6 +1296,11 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_stock_inventory_count.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite('scenario_stock_shipment_internal.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="product"/>
<field name="product"/>
<newline/>
<label name="quantity_resulting"/>
<field name="quantity_resulting"/>
<label name="uom"/>
<field name="uom"/>
<label name="quantity_added"/>
<field name="quantity_added"/>
<field name="line" colspan="-1" invisible="1"/>
</form>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form col="2">
<label name="search"/>
<field name="search"/>
</form>
+3 -3
View File
@@ -12,8 +12,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="lost_found"/>
<label name="company"/>
<field name="company"/>
<newline/>
<button name="complete_lines" colspan="2"/>
<label name="empty_quantity"/>
<field name="empty_quantity"/>
<field name="lines" colspan="4"/>
@@ -21,7 +19,9 @@ this repository contains the full copyright notices and license terms. -->
<label name="state"/>
<field name="state"/>
<group colspan="2" col="-1" id="buttons">
<button name="cancel" icon="tryton-cancel" />
<button name="cancel" icon="tryton-cancel"/>
<button name="complete_lines"/>
<button name="count"/>
<button name="confirm" icon="tryton-ok"/>
</group>
</group>