Add count wizard on inventory
issue7515 review49461002
This commit is contained in:
1 parent
973dd92330
commit
e5852ab668
10 files changed
+344
-14
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user