Add stock inventory and turnover reports
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add inventory and turnover reports
|
||||
|
||||
Version 7.6.0 - 2025-04-28
|
||||
--------------------------
|
||||
|
||||
@@ -11,3 +11,4 @@ The stock module introduces or extends the following concepts.
|
||||
.. include:: period.inc.rst
|
||||
.. include:: configuration.inc.rst
|
||||
.. include:: product.inc.rst
|
||||
.. include:: reporting.inc.rst
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
.. _model-stock.reporting.inventory:
|
||||
|
||||
Inventory Reporting
|
||||
===================
|
||||
|
||||
The *Inventory* report is used to calculate the quantity of `Product
|
||||
<product:concept-product>` at a given `Location <model-stock.location>` on a
|
||||
specific date.
|
||||
|
||||
.. seealso::
|
||||
|
||||
Inventory report can be accessed from the main menu item:
|
||||
|
||||
|Inventory & Stock --> Reporting --> Inventory|__
|
||||
|
||||
.. |Inventory & Stock --> Reporting --> Inventory| replace:: :menuselection:`Inventory & Stock --> Reporting --> Inventory`
|
||||
__ https://demo.tryton.org/model/stock.reporting.inventory
|
||||
|
||||
.. _model-stock.reporting.inventory.move:
|
||||
|
||||
Inventory Moves Reporting
|
||||
=========================
|
||||
|
||||
The *Inventory Moves* report provides information about how `Stock Moves
|
||||
<model-stock.move>` have affected the stock levels in a `Location
|
||||
<model-stock.location>` over time.
|
||||
|
||||
.. seealso::
|
||||
|
||||
Inventory Moves report can be accessed from the :guilabel:`Stock Inventory
|
||||
Moves` in the product's :guilabel:`Open related records` menu.
|
||||
|
||||
.. _model-stock.reporting.inventory.daily:
|
||||
|
||||
Inventory Daily Reporting
|
||||
=========================
|
||||
|
||||
The *Inventory Daily* report provides information about how the stock levels of
|
||||
one or more products have varied over time in a `Location
|
||||
<model-stock.location>`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
Inventory Daily report can be accessed from the :guilabel:`Stock Inventory
|
||||
Daily` in the product's :guilabel:`Open related records` menu.
|
||||
|
||||
.. _model-stock.reporting.inventory.turnover:
|
||||
|
||||
Inventory Turnover Reporting
|
||||
============================
|
||||
|
||||
The *Inventory Turnover* report calculates the turnover of each `Product
|
||||
<product:concept-product>` of a `Location <model-stock.location>` over a
|
||||
period.
|
||||
|
||||
.. seealso::
|
||||
|
||||
Inventory Turnover report can be accessed from the main menu item:
|
||||
|
||||
|Inventory & Stock --> Reporting --> Inventory Turnover|__
|
||||
|
||||
.. |Inventory & Stock --> Reporting --> Inventory Turnover| replace:: :menuselection:`Inventory & Stock --> Reporting --> Inventory Turnover`
|
||||
__ https://demo.tryton.org/model/stock.reporting.inventory.turnover
|
||||
@@ -443,6 +443,14 @@ class Move(Workflow, ModelSQL, ModelView):
|
||||
(t.company, Index.Equality()),
|
||||
(t.state, Index.Equality(cardinality='low')),
|
||||
where=t.state.in_(['staging', 'draft', 'assigned'])),
|
||||
Index(
|
||||
t,
|
||||
(t.company, Index.Equality()),
|
||||
(Coalesce(t.effective_date, t.planned_date),
|
||||
Index.Range()),
|
||||
(t.state, Index.Equality(cardinality='low')),
|
||||
(t.product, Index.Range()),
|
||||
where=t.state.in_(['done', 'assigned', 'draft'])),
|
||||
})
|
||||
cls._order[0] = ('id', 'DESC')
|
||||
cls._transitions |= set((
|
||||
|
||||
@@ -71,7 +71,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
parent="menu_stock"
|
||||
sequence="100"
|
||||
id="menu_reporting"
|
||||
active="0"/>
|
||||
active="1"/>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from sql import Literal, Null, Window
|
||||
from sql.aggregate import Min, Sum
|
||||
from sql.conditionals import Case, Coalesce, Greatest, Least, NullIf
|
||||
from sql.functions import Function, NthValue, Round
|
||||
from sql.operators import Concat
|
||||
|
||||
from trytond import backend
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class _SQLite_JulianDay(Function):
|
||||
__slots__ = ()
|
||||
_function = 'JULIANDAY'
|
||||
|
||||
|
||||
class _InventoryContextMixin(ModelView):
|
||||
|
||||
company = fields.Many2One('company.company', "Company", required=True)
|
||||
location = fields.Many2One(
|
||||
'stock.location', "Location", required=True,
|
||||
domain=[
|
||||
('type', 'in', ['warehouse', 'storage', 'view']),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_location(cls):
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
return Transaction().context.get(
|
||||
'location', Location.get_default_warehouse())
|
||||
|
||||
|
||||
class _InventoryMixin:
|
||||
__slots__ = ()
|
||||
|
||||
company = fields.Many2One('company.company', "Company")
|
||||
product = fields.Reference("Product", [
|
||||
('product.product', "Variant"),
|
||||
('product.template', "Product"),
|
||||
])
|
||||
unit = fields.Function(
|
||||
fields.Many2One('product.uom', "Unit"),
|
||||
'on_change_with_unit')
|
||||
|
||||
input_quantity = fields.Float("Input Quantity")
|
||||
output_quantity = fields.Float("Output Quantity")
|
||||
quantity = fields.Float("Quantity")
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
Move = pool.get('stock.move')
|
||||
Period = pool.get('stock.period')
|
||||
PeriodCache = pool.get('stock.period.cache')
|
||||
Product = pool.get('product.product')
|
||||
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
move = Move.__table__()
|
||||
from_location = Location.__table__()
|
||||
to_location = Location.__table__()
|
||||
period_cache = PeriodCache.__table__()
|
||||
product_table = Product.__table__()
|
||||
|
||||
if location := context.get('location'):
|
||||
location = Location(location)
|
||||
left, right = location.left, location.right
|
||||
else:
|
||||
left = right = -1
|
||||
if date := context.get('date'):
|
||||
from_date = to_date = date
|
||||
else:
|
||||
from_date = context.get('from_date') or dt.date.min
|
||||
to_date = context.get('to_date') or dt.date.max
|
||||
company = context.get('company')
|
||||
|
||||
if periods := Period.search([
|
||||
('company', '=', company),
|
||||
('date', '<=', from_date),
|
||||
('state', '=', 'closed'),
|
||||
],
|
||||
order=[('date', 'DESC')],
|
||||
limit=1):
|
||||
period, = periods
|
||||
else:
|
||||
period = None
|
||||
|
||||
quantities = (
|
||||
move
|
||||
.join(from_location,
|
||||
condition=move.from_location == from_location.id)
|
||||
.join(to_location,
|
||||
condition=move.to_location == to_location.id)
|
||||
)
|
||||
|
||||
if context.get('product_type') == 'product.product':
|
||||
product = Concat('product.product,', move.product)
|
||||
else:
|
||||
product = Concat('product.template,', product_table.template)
|
||||
quantities = (
|
||||
quantities
|
||||
.join(product_table,
|
||||
condition=move.product == product_table.id)
|
||||
)
|
||||
|
||||
move_date = Coalesce(move.effective_date, move.planned_date)
|
||||
input_clause = (
|
||||
(to_location.left >= left) & (to_location.right <= right))
|
||||
output_clause = (
|
||||
(from_location.left >= left) & (from_location.right <= right))
|
||||
start_quantity = Sum(
|
||||
move.quantity * Case((output_clause, -1), else_=1),
|
||||
filter_=(move_date < from_date))
|
||||
input_quantity = Sum(
|
||||
move.quantity,
|
||||
filter_=input_clause & (move_date >= from_date))
|
||||
output_quantity = Sum(
|
||||
move.quantity,
|
||||
filter_=output_clause & (move_date >= from_date))
|
||||
|
||||
date_column = Greatest(move_date, from_date)
|
||||
if context.get('product_type') == 'product.product':
|
||||
partition = [move.product]
|
||||
else:
|
||||
partition = [product_table.template]
|
||||
|
||||
# Use NthValue instead of LastValue to get NULL for the last row
|
||||
next_date_column = NthValue(
|
||||
date_column, 2, window=Window(
|
||||
partition,
|
||||
order_by=[date_column.asc],
|
||||
frame='ROWS', start=0, end=1))
|
||||
|
||||
state_clause = cls._state_clause(move, to_date)
|
||||
|
||||
quantities = quantities.select(
|
||||
Min(move.id).as_('id'),
|
||||
product.as_('product'),
|
||||
date_column.as_('date'),
|
||||
next_date_column.as_('next_date'),
|
||||
start_quantity.as_('start_quantity'),
|
||||
input_quantity.as_('input_quantity'),
|
||||
output_quantity.as_('output_quantity'),
|
||||
*cls._quantities_columns(move, product_table),
|
||||
where=(((input_clause & ~output_clause)
|
||||
| (output_clause & ~input_clause))
|
||||
& (move_date <= to_date)
|
||||
& state_clause
|
||||
& (move.company == company)),
|
||||
group_by=[
|
||||
*partition,
|
||||
date_column,
|
||||
*cls._quantities_group_by(move, product_table),
|
||||
],
|
||||
)
|
||||
|
||||
quantity = Sum(
|
||||
Coalesce(quantities.start_quantity, 0)
|
||||
+ Coalesce(quantities.input_quantity, 0)
|
||||
- Coalesce(quantities.output_quantity, 0),
|
||||
window=Window(
|
||||
[quantities.product],
|
||||
order_by=[*cls._quantities_order_by(quantities)]))
|
||||
|
||||
if period:
|
||||
quantities.where &= move_date > period.date
|
||||
if context.get('product_type') != 'product.product':
|
||||
period_cache_product = Product.__table__()
|
||||
cache = (
|
||||
period_cache
|
||||
.join(period_cache_product,
|
||||
condition=period_cache.product
|
||||
== period_cache_product.id)
|
||||
.select(
|
||||
period_cache_product.template.as_('product'),
|
||||
Sum(period_cache.internal_quantity
|
||||
).as_('quantity'),
|
||||
group_by=[period_cache_product.template]))
|
||||
else:
|
||||
cache = (
|
||||
period_cache
|
||||
.select(
|
||||
period_cache.product.as_('product'),
|
||||
Sum(period_cache.internal_quantity).as_('quantity'),
|
||||
group_by=[period_cache.product]))
|
||||
location_cache = Location.__table__()
|
||||
cache.where = (
|
||||
(period_cache.period == period.id)
|
||||
& period_cache.location.in_(
|
||||
location_cache.select(
|
||||
location_cache.id,
|
||||
where=(location_cache.left >= left)
|
||||
& (location_cache.right <= right))))
|
||||
query = quantities.join(cache, 'LEFT',
|
||||
condition=(
|
||||
cache.product
|
||||
== cls.product.sql_id(quantities.product, cls)))
|
||||
quantity += Coalesce(cache.quantity, 0)
|
||||
else:
|
||||
query = quantities
|
||||
|
||||
return (query
|
||||
.select(
|
||||
quantities.id.as_('id'),
|
||||
Literal(company).as_('company'),
|
||||
quantities.product.as_('product'),
|
||||
quantities.input_quantity.as_('input_quantity'),
|
||||
quantities.output_quantity.as_('output_quantity'),
|
||||
quantity.as_('quantity'),
|
||||
*cls._columns(quantities),
|
||||
where=(
|
||||
(Coalesce(quantities.next_date, dt.date.max) >= from_date)
|
||||
| (quantities.date >= from_date))))
|
||||
|
||||
@classmethod
|
||||
def _quantities_columns(cls, move, product):
|
||||
yield from []
|
||||
|
||||
@classmethod
|
||||
def _quantities_group_by(cls, move, product):
|
||||
yield from []
|
||||
|
||||
@classmethod
|
||||
def _quantities_order_by(cls, quantities):
|
||||
yield quantities.date.asc
|
||||
|
||||
@classmethod
|
||||
def _columns(cls, quantities):
|
||||
yield from []
|
||||
|
||||
@classmethod
|
||||
def _where(cls, quantities):
|
||||
return Literal(True)
|
||||
|
||||
@classmethod
|
||||
def _state_clause(cls, move, date):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
today = Date.today()
|
||||
forcast = date > today
|
||||
|
||||
move_date = Coalesce(move.effective_date, move.planned_date)
|
||||
state_clause = (
|
||||
(move_date <= today) & (move.state == 'done'))
|
||||
state_clause |= (
|
||||
((move_date >= today) if forcast else (move_date > today))
|
||||
& (move.state.in_(['done', 'assigned', 'draft'])))
|
||||
return state_clause
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_unit(self, name=None):
|
||||
if self.product:
|
||||
return self.product.default_uom
|
||||
|
||||
|
||||
class InventoryContext(_InventoryContextMixin):
|
||||
__name__ = 'stock.reporting.inventory.context'
|
||||
|
||||
date = fields.Date("Date", required=True)
|
||||
product_type = fields.Selection([
|
||||
('product.product', "Variant"),
|
||||
('product.template', "Product"),
|
||||
], "Product Type", required=True)
|
||||
|
||||
@classmethod
|
||||
def default_date(cls):
|
||||
return Pool().get('ir.date').today()
|
||||
|
||||
@classmethod
|
||||
def default_product_type(cls):
|
||||
return Transaction().context.get('product_type') or 'product.template'
|
||||
|
||||
|
||||
class Inventory(_InventoryMixin, ModelSQL, ModelView):
|
||||
__name__ = 'stock.reporting.inventory'
|
||||
|
||||
|
||||
class _InventoryRangeContextMixin(_InventoryContextMixin):
|
||||
|
||||
from_date = fields.Date(
|
||||
"From Date", required=True,
|
||||
domain=[
|
||||
('from_date', '<=', Eval('to_date')),
|
||||
])
|
||||
to_date = fields.Date(
|
||||
"To Date", required=True,
|
||||
domain=[
|
||||
('to_date', '>=', Eval('from_date')),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def default_from_date(cls):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
context = Transaction().context
|
||||
if 'from_date' in context:
|
||||
return context['from_date']
|
||||
return Date.today() - relativedelta(day=1, months=6)
|
||||
|
||||
@classmethod
|
||||
def default_to_date(cls):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
context = Transaction().context
|
||||
if 'to_date' in context:
|
||||
return context['to_date']
|
||||
return Date.today() + relativedelta(day=1, months=6)
|
||||
|
||||
|
||||
class InventoryRangeContext(_InventoryRangeContextMixin):
|
||||
__name__ = 'stock.reporting.inventory.range.context'
|
||||
|
||||
|
||||
class InventoryMove(_InventoryMixin, ModelSQL, ModelView):
|
||||
__name__ = 'stock.reporting.inventory.move'
|
||||
|
||||
date = fields.Date("Date")
|
||||
move = fields.Many2One('stock.move', "Move")
|
||||
origin = fields.Reference("Origin", selection='get_origin')
|
||||
document = fields.Function(
|
||||
fields.Reference("Document", selection='get_documents'),
|
||||
'get_document')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order = [
|
||||
('date', 'DESC'),
|
||||
('move', 'DESC NULLS LAST'),
|
||||
('id', 'DESC'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _quantities_columns(cls, move, product):
|
||||
from_date = Transaction().context.get('from_date') or dt.date.min
|
||||
move_date = Coalesce(move.effective_date, move.planned_date)
|
||||
yield from super()._quantities_columns(move, product)
|
||||
yield Case(
|
||||
(move_date < from_date, Null),
|
||||
else_=move.id).as_('move')
|
||||
yield Case(
|
||||
(move_date < from_date, Null),
|
||||
else_=move.origin).as_('origin')
|
||||
|
||||
@classmethod
|
||||
def _quantities_order_by(cls, quantities):
|
||||
yield from super()._quantities_order_by(quantities)
|
||||
yield quantities.move.asc.nulls_first
|
||||
|
||||
@classmethod
|
||||
def _quantities_group_by(cls, move, product):
|
||||
from_date = Transaction().context.get('from_date') or dt.date.min
|
||||
move_date = Coalesce(move.effective_date, move.planned_date)
|
||||
yield from super()._quantities_group_by(move, product)
|
||||
yield Case(
|
||||
(move_date < from_date, Null),
|
||||
else_=move.id)
|
||||
yield Case(
|
||||
(move_date < from_date, Null),
|
||||
else_=move.origin)
|
||||
|
||||
@classmethod
|
||||
def _columns(cls, quantities):
|
||||
yield from super()._columns(quantities)
|
||||
yield quantities.date.as_('date')
|
||||
yield quantities.move.as_('move')
|
||||
yield quantities.origin.as_('origin')
|
||||
|
||||
@classmethod
|
||||
def get_origin(cls):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
return Move.get_origin()
|
||||
|
||||
@classmethod
|
||||
def _get_document_models(cls):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
return [m for m, _ in Move.get_shipment() if m]
|
||||
|
||||
@classmethod
|
||||
def get_documents(cls):
|
||||
pool = Pool()
|
||||
Model = pool.get('ir.model')
|
||||
get_name = Model.get_name
|
||||
models = cls._get_document_models()
|
||||
return [(None, '')] + [(m, get_name(m)) for m in models]
|
||||
|
||||
def get_document(self, name):
|
||||
if self.move and self.move.shipment:
|
||||
return str(self.move.shipment)
|
||||
|
||||
def get_rec_name(self, name):
|
||||
name = super().get_rec_name(name)
|
||||
if self.move:
|
||||
name = self.move.rec_name
|
||||
return name
|
||||
|
||||
|
||||
class InventoryDaily(_InventoryMixin, ModelSQL, ModelView):
|
||||
__name__ = 'stock.reporting.inventory.daily'
|
||||
|
||||
from_date = fields.Date("From Date")
|
||||
to_date = fields.Date("To Date")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order = [
|
||||
('from_date', 'DESC'),
|
||||
('id', 'DESC'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _quantities_order_by(cls, quantities):
|
||||
yield from []
|
||||
yield quantities.date.asc
|
||||
|
||||
@classmethod
|
||||
def _columns(cls, quantities):
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
to_date = context.get('to_date') or dt.date.max
|
||||
|
||||
yield from super()._columns(quantities)
|
||||
|
||||
yield quantities.date.as_('from_date')
|
||||
yield Least(quantities.next_date, to_date).as_('to_date')
|
||||
|
||||
|
||||
class InventoryTurnoverContext(_InventoryRangeContextMixin):
|
||||
__name__ = 'stock.reporting.inventory.turnover.context'
|
||||
|
||||
product_type = fields.Selection([
|
||||
('product.product', "Variant"),
|
||||
('product.template', "Product"),
|
||||
], "Product Type", required=True)
|
||||
|
||||
@classmethod
|
||||
def default_product_type(cls):
|
||||
return Transaction().context.get('product_type') or 'product.template'
|
||||
|
||||
|
||||
class InventoryTurnover(ModelSQL, ModelView):
|
||||
__name__ = 'stock.reporting.inventory.turnover'
|
||||
|
||||
company = fields.Many2One('company.company', "Company")
|
||||
product = fields.Reference("Product", [
|
||||
('product.product', "Variant"),
|
||||
('product.template', "Product"),
|
||||
])
|
||||
unit = fields.Function(
|
||||
fields.Many2One('product.uom', "Unit"),
|
||||
'on_change_with_unit')
|
||||
|
||||
output_quantity = fields.Float("Output Quantity", digits=(None, 3))
|
||||
average_quantity = fields.Float("Average Quantity", digits=(None, 3))
|
||||
turnover = fields.Float("Turnover", digits=(None, 3))
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
pool = Pool()
|
||||
Inventory = pool.get('stock.reporting.inventory.daily')
|
||||
|
||||
transaction = Transaction()
|
||||
context = transaction.context
|
||||
inventory = Inventory.__table__()
|
||||
|
||||
from_date = context.get('from_date') or dt.date.min
|
||||
to_date = context.get('to_date') or dt.date.max
|
||||
company = context.get('company')
|
||||
|
||||
days = (to_date - from_date).days + 1
|
||||
|
||||
output_quantity = Sum(inventory.output_quantity) / days
|
||||
inventory_from_date = inventory.from_date
|
||||
inventory_to_date = inventory.to_date
|
||||
if backend.name == 'sqlite':
|
||||
inventory_from_date = _SQLite_JulianDay(inventory_from_date)
|
||||
inventory_to_date = _SQLite_JulianDay(inventory_to_date)
|
||||
average_quantity = (
|
||||
Sum(Case(
|
||||
(inventory.quantity >= 0, inventory.quantity),
|
||||
else_=0)
|
||||
* (inventory_to_date - inventory_from_date
|
||||
+ Case((inventory.to_date == to_date, 1), else_=0)))
|
||||
/ days)
|
||||
|
||||
def round_sql(expression, digits=2):
|
||||
factor = 10 ** digits
|
||||
return Round(expression * factor) / factor
|
||||
|
||||
return (inventory
|
||||
.select(
|
||||
cls.product.sql_id(inventory.product, cls).as_('id'),
|
||||
Literal(company).as_('company'),
|
||||
inventory.product.as_('product'),
|
||||
round_sql(
|
||||
output_quantity,
|
||||
cls.output_quantity.digits[1]).as_('output_quantity'),
|
||||
round_sql(
|
||||
average_quantity,
|
||||
cls.average_quantity.digits[1]).as_('average_quantity'),
|
||||
round_sql(
|
||||
output_quantity / NullIf(average_quantity, 0),
|
||||
cls.turnover.digits[1]).as_('turnover'),
|
||||
group_by=[inventory.product]))
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_unit(self, name=None):
|
||||
if self.product:
|
||||
return self.product.default_uom
|
||||
@@ -0,0 +1,315 @@
|
||||
<?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. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="reporting_inventory_context_view_form">
|
||||
<field name="model">stock.reporting.inventory.context</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_context_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_view_list">
|
||||
<field name="model">stock.reporting.inventory</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">reporting_inventory_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_view_form">
|
||||
<field name="model">stock.reporting.inventory</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_reporting_inventory">
|
||||
<field name="name">Inventory</field>
|
||||
<field name="res_model">stock.reporting.inventory</field>
|
||||
<field name="context_model">stock.reporting.inventory.context</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="reporting_inventory_view_list"/>
|
||||
<field name="act_window" ref="act_reporting_inventory"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="reporting_inventory_view_form"/>
|
||||
<field name="act_window" ref="act_reporting_inventory"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_reporting"
|
||||
action="act_reporting_inventory"
|
||||
sequence="20"
|
||||
id="menu_reporting_inventory"/>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_reporting_inventory_companies">
|
||||
<field name="name">User in companies</field>
|
||||
<field name="model">stock.reporting.inventory</field>
|
||||
<field name="global_p" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_reporting_inventory_companies">
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('company', 'in', Eval('companies', []))]"
|
||||
pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_reporting_inventory_companies"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_reporting_inventory">
|
||||
<field name="model">stock.reporting.inventory</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_reporting_inventory_group_stock">
|
||||
<field name="model">stock.reporting.inventory</field>
|
||||
<field name="group" ref="group_stock"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_range_context_view_form">
|
||||
<field name="model">stock.reporting.inventory.range.context</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_range_context_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_move_view_list">
|
||||
<field name="model">stock.reporting.inventory.move</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">reporting_inventory_move_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_move_view_form">
|
||||
<field name="model">stock.reporting.inventory.move</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_move_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_reporting_inventory_move">
|
||||
<field name="name">Stock Inventory Moves</field>
|
||||
<field name="res_model">stock.reporting.inventory.move</field>
|
||||
<field name="context_model">stock.reporting.inventory.range.context</field>
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('product', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
|
||||
pyson="1"/>
|
||||
<field
|
||||
name="context"
|
||||
eval="{'product_type': Eval('active_model', 'product.template'), 'date': None}"
|
||||
pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_move_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="reporting_inventory_move_view_list"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_move"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_move_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="reporting_inventory_move_view_form"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_move"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_reporting_inventory_move_keyword_product">
|
||||
<field name="keyword">form_relate</field>
|
||||
<field name="model">product.product,-1</field>
|
||||
<field name="action" ref="act_reporting_inventory_move"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_reporting_inventory_move_keyword_template">
|
||||
<field name="keyword">form_relate</field>
|
||||
<field name="model">product.template,-1</field>
|
||||
<field name="action" ref="act_reporting_inventory_move"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_reporting_inventory_move_companies">
|
||||
<field name="name">User in companies</field>
|
||||
<field name="model">stock.reporting.inventory.move</field>
|
||||
<field name="global_p" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_reporting_inventory_move_companies">
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('company', 'in', Eval('companies', []))]"
|
||||
pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_reporting_inventory_move_companies"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_reporting_inventory_move">
|
||||
<field name="model">stock.reporting.inventory.move</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_reporting_inventory_move_group_stock">
|
||||
<field name="model">stock.reporting.inventory.move</field>
|
||||
<field name="group" ref="group_stock"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_daily_view_list">
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">reporting_inventory_daily_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_daily_view_form">
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_daily_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_daily_view_graph">
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="type">graph</field>
|
||||
<field name="name">reporting_inventory_daily_graph</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_reporting_inventory_daily">
|
||||
<field name="name">Stock Inventory Daily</field>
|
||||
<field name="res_model">stock.reporting.inventory.daily</field>
|
||||
<field name="context_model">stock.reporting.inventory.range.context</field>
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('product', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
|
||||
pyson="1"/>
|
||||
<field
|
||||
name="context"
|
||||
eval="{'product_type': Eval('active_model', 'product.template'), 'date': None}"
|
||||
pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_daily_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="reporting_inventory_daily_view_list"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_daily"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_daily_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="reporting_inventory_daily_view_graph"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_daily"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_daily_view3">
|
||||
<field name="sequence" eval="30"/>
|
||||
<field name="view" ref="reporting_inventory_daily_view_form"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_daily"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_reporting_inventory_daily_keyword_product">
|
||||
<field name="keyword">form_relate</field>
|
||||
<field name="model">product.product,-1</field>
|
||||
<field name="action" ref="act_reporting_inventory_daily"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_reporting_inventory_daily_keyword_template">
|
||||
<field name="keyword">form_relate</field>
|
||||
<field name="model">product.template,-1</field>
|
||||
<field name="action" ref="act_reporting_inventory_daily"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_reporting_inventory_daily_companies">
|
||||
<field name="name">User in companies</field>
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="global_p" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_reporting_inventory_daily_companies">
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('company', 'in', Eval('companies', []))]"
|
||||
pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_reporting_inventory_daily_companies"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_reporting_inventory_daily">
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_reporting_inventory_daily_group_stock">
|
||||
<field name="model">stock.reporting.inventory.daily</field>
|
||||
<field name="group" ref="group_stock"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_turnover_context_view_form">
|
||||
<field name="model">stock.reporting.inventory.turnover.context</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_turnover_context_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_turnover_view_list">
|
||||
<field name="model">stock.reporting.inventory.turnover</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">reporting_inventory_turnover_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="reporting_inventory_turnover_view_form">
|
||||
<field name="model">stock.reporting.inventory.turnover</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">reporting_inventory_turnover_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_reporting_inventory_turnover">
|
||||
<field name="name">Inventory Turnover</field>
|
||||
<field name="res_model">stock.reporting.inventory.turnover</field>
|
||||
<field name="context_model">stock.reporting.inventory.turnover.context</field>
|
||||
<field
|
||||
name="context"
|
||||
eval="{'from_date': Date(delta_years=-1), 'to_date': Date()}"
|
||||
pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_turnover_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="reporting_inventory_turnover_view_list"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_turnover"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_reporting_inventory_turnover_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="reporting_inventory_turnover_view_form"/>
|
||||
<field name="act_window" ref="act_reporting_inventory_turnover"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_reporting"
|
||||
action="act_reporting_inventory_turnover"
|
||||
sequence="30"
|
||||
id="menu_reporting_inventory_turnover"/>
|
||||
|
||||
<record model="ir.rule.group" id="rule_group_reporting_inventory_turnover_companies">
|
||||
<field name="name">User in companies</field>
|
||||
<field name="model">stock.reporting.inventory.turnover</field>
|
||||
<field name="global_p" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="rule_reporting_inventory_turnover_companies">
|
||||
<field
|
||||
name="domain"
|
||||
eval="[('company', 'in', Eval('companies', []))]"
|
||||
pyson="1"/>
|
||||
<field name="rule_group" ref="rule_group_reporting_inventory_turnover_companies"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_reporting_inventory_turnover">
|
||||
<field name="model">stock.reporting.inventory.turnover</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_reporting_inventory_turnover_group_stock">
|
||||
<field name="model">stock.reporting.inventory.turnover</field>
|
||||
<field name="group" ref="group_stock"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"period_close": false, "product_type": "product.template"}
|
||||
,{"period_close": false, "product_type": "product.product"}
|
||||
,{"period_close": true, "product_type": "product.template"}
|
||||
,{"period_close": true, "product_type": "product.product"}
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
==================================
|
||||
Stock Reporting Inventory Scenario
|
||||
==================================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> from decimal import Decimal
|
||||
>>> from unittest.mock import patch
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.ir.date import Date
|
||||
>>> from trytond.modules.company.tests.tools import create_company
|
||||
>>> from trytond.modules.currency.tests.tools import get_currency
|
||||
>>> from trytond.tests.tools import activate_modules, assertEqual
|
||||
|
||||
>>> period_close = globals().get('period_close', False)
|
||||
>>> product_type = globals().get('product_type', 'product.template')
|
||||
|
||||
Patch today::
|
||||
|
||||
>>> mock = patch.object(Date, 'today', return_value=dt.date(2025, 1, 15))
|
||||
>>> _ = mock.start()
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('stock', create_company)
|
||||
|
||||
>>> Inventory = Model.get('stock.reporting.inventory')
|
||||
>>> InventoryMove = Model.get('stock.reporting.inventory.move')
|
||||
>>> InventoryDaily = Model.get('stock.reporting.inventory.daily')
|
||||
>>> InventoryTurnover = Model.get('stock.reporting.inventory.turnover')
|
||||
>>> Location = Model.get('stock.location')
|
||||
>>> Move = Model.get('stock.move')
|
||||
>>> Period = Model.get('stock.period')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> ProductUom = Model.get('product.uom')
|
||||
|
||||
Get currency::
|
||||
|
||||
>>> currency = get_currency()
|
||||
|
||||
Create product::
|
||||
|
||||
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = 'Product'
|
||||
>>> template.default_uom = unit
|
||||
>>> template.type = 'goods'
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
|
||||
Get stock locations::
|
||||
|
||||
>>> warehouse_loc, = Location.find([('code', '=', 'WH')])
|
||||
>>> supplier_loc, = Location.find([('code', '=', 'SUP')])
|
||||
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
|
||||
>>> storage_loc, = Location.find([('code', '=', 'STO')])
|
||||
|
||||
Fill warehouse::
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.product = product
|
||||
>>> move.from_location = supplier_loc
|
||||
>>> move.to_location = storage_loc
|
||||
>>> move.quantity = 10
|
||||
>>> move.effective_date = dt.date(2025, 1, 1)
|
||||
>>> move.unit_price = Decimal('10.0000')
|
||||
>>> move.currency = currency
|
||||
>>> move.click('do')
|
||||
>>> move.state
|
||||
'done'
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.product = product
|
||||
>>> move.from_location = storage_loc
|
||||
>>> move.to_location = customer_loc
|
||||
>>> move.unit = unit
|
||||
>>> move.quantity = 1
|
||||
>>> move.effective_date = dt.date(2025, 1, 1)
|
||||
>>> move.unit_price = Decimal('20.0000')
|
||||
>>> move.currency = currency
|
||||
>>> move.click('do')
|
||||
>>> move.state
|
||||
'done'
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.product = product
|
||||
>>> move.from_location = storage_loc
|
||||
>>> move.to_location = customer_loc
|
||||
>>> move.unit = unit
|
||||
>>> move.quantity = 2
|
||||
>>> move.effective_date = dt.date(2025, 1, 10)
|
||||
>>> move.unit_price = Decimal('20.0000')
|
||||
>>> move.currency = currency
|
||||
>>> move.click('do')
|
||||
>>> move.state
|
||||
'done'
|
||||
|
||||
Forecast some moves::
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.product = product
|
||||
>>> move.from_location = supplier_loc
|
||||
>>> move.to_location = storage_loc
|
||||
>>> move.quantity = 10
|
||||
>>> move.effective_date = dt.date(2025, 1, 5)
|
||||
>>> move.unit_price = Decimal('10.0000')
|
||||
>>> move.currency = currency
|
||||
>>> move.save()
|
||||
>>> move.state
|
||||
'draft'
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.product = product
|
||||
>>> move.from_location = storage_loc
|
||||
>>> move.to_location = customer_loc
|
||||
>>> move.unit = unit
|
||||
>>> move.quantity = 3
|
||||
>>> move.planned_date = dt.date(2025, 1, 20)
|
||||
>>> move.unit_price = Decimal('20.0000')
|
||||
>>> move.currency = currency
|
||||
>>> move.save()
|
||||
>>> move.state
|
||||
'draft'
|
||||
|
||||
Close period::
|
||||
|
||||
>>> period = Period(date=dt.date(2025, 1, 5))
|
||||
>>> if period_close:
|
||||
... period.click('close')
|
||||
|
||||
|
||||
Check inventory::
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... product_type=product_type,
|
||||
... date=dt.date(2024, 1, 1)):
|
||||
... Inventory.find([])
|
||||
[]
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... product_type=product_type,
|
||||
... date=dt.date(2025, 1, 1)):
|
||||
... inventory, = Inventory.find([])
|
||||
>>> inventory.quantity
|
||||
9.0
|
||||
>>> assertEqual(inventory.product.__class__.__name__, product_type)
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... product_type=product_type,
|
||||
... date=dt.date(2025, 1, 15)):
|
||||
... inventory, = Inventory.find([])
|
||||
>>> inventory.quantity
|
||||
7.0
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... product_type=product_type,
|
||||
... date=dt.date(2025, 1, 20)):
|
||||
... inventory, = Inventory.find([])
|
||||
>>> inventory.quantity
|
||||
4.0
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... product_type=product_type):
|
||||
... inventory_moves = InventoryMove.find([])
|
||||
... inventories_daily = InventoryDaily.find([])
|
||||
|
||||
>>> [i.quantity for i in inventory_moves]
|
||||
[4.0, 7.0, 9.0, 10.0]
|
||||
>>> [(i.input_quantity, i.output_quantity) for i in inventory_moves]
|
||||
[(None, 3.0), (None, 2.0), (None, 1.0), (10.0, None)]
|
||||
|
||||
>>> [i.quantity for i in inventories_daily]
|
||||
[4.0, 7.0, 9.0]
|
||||
>>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
|
||||
[(None, 3.0), (None, 2.0), (10.0, 1.0)]
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... from_date=dt.date(2025, 1, 15),
|
||||
... to_date=dt.date(2025, 1, 29),
|
||||
... product_type=product_type):
|
||||
... inventory_moves = InventoryMove.find([])
|
||||
... inventories_daily = InventoryDaily.find([])
|
||||
|
||||
>>> [i.quantity for i in inventory_moves]
|
||||
[4.0, 7.0]
|
||||
>>> [(i.input_quantity, i.output_quantity) for i in inventory_moves]
|
||||
[(None, 3.0), (None, None)]
|
||||
|
||||
>>> [i.quantity for i in inventories_daily]
|
||||
[4.0, 7.0]
|
||||
>>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
|
||||
[(None, 3.0), (None, None)]
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... from_date=dt.date(2025, 1, 10),
|
||||
... to_date=dt.date(2025, 1, 29),
|
||||
... product_type=product_type):
|
||||
... inventory_moves = InventoryMove.find([])
|
||||
... inventories_daily = InventoryDaily.find([])
|
||||
|
||||
>>> assertEqual(
|
||||
... [i.quantity for i in inventory_moves],
|
||||
... [4.0, 7.0] if period_close else [4.0, 7.0, 9.0])
|
||||
>>> assertEqual(
|
||||
... [(i.input_quantity, i.output_quantity) for i in inventory_moves],
|
||||
... [(None, 3.0), (None, 2.0)] if period_close
|
||||
... else [(None, 3.0), (None, 2.0), (None, None)])
|
||||
|
||||
>>> [i.quantity for i in inventories_daily]
|
||||
[4.0, 7.0]
|
||||
>>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
|
||||
[(None, 3.0), (None, 2.0)]
|
||||
|
||||
Check Inventory turnover::
|
||||
|
||||
>>> with config.set_context(
|
||||
... location=warehouse_loc.id,
|
||||
... from_date=dt.date(2025, 1, 10),
|
||||
... to_date=dt.date(2025, 1, 29),
|
||||
... product_type=product_type):
|
||||
... turnover, = InventoryTurnover.find([])
|
||||
|
||||
>>> turnover.output_quantity
|
||||
0.25
|
||||
>>> turnover.average_quantity
|
||||
5.5
|
||||
>>> turnover.turnover
|
||||
0.045
|
||||
>>> assertEqual(turnover.product.__class__.__name__, product_type)
|
||||
@@ -19,6 +19,7 @@ xml:
|
||||
period.xml
|
||||
message.xml
|
||||
res.xml
|
||||
stock_reporting.xml
|
||||
stock_reporting_margin.xml
|
||||
|
||||
[register]
|
||||
@@ -60,6 +61,13 @@ model:
|
||||
inventory.InventoryLine
|
||||
inventory.CountSearch
|
||||
inventory.CountQuantity
|
||||
stock_reporting.InventoryContext
|
||||
stock_reporting.Inventory
|
||||
stock_reporting.InventoryRangeContext
|
||||
stock_reporting.InventoryMove
|
||||
stock_reporting.InventoryDaily
|
||||
stock_reporting.InventoryTurnoverContext
|
||||
stock_reporting.InventoryTurnover
|
||||
stock_reporting_margin.Context
|
||||
stock_reporting_margin.Main
|
||||
stock_reporting_margin.MainTimeseries
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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="date"/>
|
||||
<field name="date"/>
|
||||
<label name="product_type"/>
|
||||
<field name="product_type"/>
|
||||
|
||||
<label name="location"/>
|
||||
<field name="location"/>
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
</form>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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" colspan="3"/>
|
||||
|
||||
<label name="from_date"/>
|
||||
<field name="from_date"/>
|
||||
<label name="to_date"/>
|
||||
<field name="to_date"/>
|
||||
|
||||
<label name="input_quantity"/>
|
||||
<field name="input_quantity" symbol="unit"/>
|
||||
<label name="output_quantity"/>
|
||||
<field name="output_quantity" symbol="unit"/>
|
||||
|
||||
<label name="quantity"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</form>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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. -->
|
||||
<graph type="line" legend="0">
|
||||
<x>
|
||||
<field name="from_date"/>
|
||||
</x>
|
||||
<y>
|
||||
<field name="quantity" empty="0" interpolation="constant-left"/>
|
||||
</y>
|
||||
</graph>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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. -->
|
||||
<tree>
|
||||
<field name="product" expand="2"/>
|
||||
<field name="from_date" string="Date"/>
|
||||
<field name="input_quantity" symbol="unit" optional="1"/>
|
||||
<field name="output_quantity" symbol="unit" optional="1"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</tree>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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" colspan="3"/>
|
||||
|
||||
<label name="quantity"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</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. -->
|
||||
<tree>
|
||||
<field name="product" expand="2"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</tree>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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" colspan="3"/>
|
||||
|
||||
<label name="date"/>
|
||||
<field name="date"/>
|
||||
<label name="move"/>
|
||||
<field name="move"/>
|
||||
|
||||
<label name="origin"/>
|
||||
<field name="origin"/>
|
||||
<label name="document"/>
|
||||
<field name="document"/>
|
||||
|
||||
<label name="input_quantity"/>
|
||||
<field name="input_quantity" symbol="unit"/>
|
||||
<label name="output_quantity"/>
|
||||
<field name="output_quantity" symbol="unit"/>
|
||||
|
||||
<label name="quantity"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</form>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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. -->
|
||||
<tree>
|
||||
<field name="product" expand="2"/>
|
||||
<field name="date"/>
|
||||
<field name="origin" expand="1" optional="1"/>
|
||||
<field name="document" expand="1" optional="0"/>
|
||||
<field name="input_quantity" symbol="unit" optional="1"/>
|
||||
<field name="output_quantity" symbol="unit" optional="1"/>
|
||||
<field name="quantity" symbol="unit"/>
|
||||
</tree>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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="from_date"/>
|
||||
<field name="from_date"/>
|
||||
<label name="to_date"/>
|
||||
<field name="to_date"/>
|
||||
|
||||
<label name="location"/>
|
||||
<field name="location"/>
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
</form>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- 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="6">
|
||||
<label name="from_date"/>
|
||||
<field name="from_date"/>
|
||||
<label name="to_date"/>
|
||||
<field name="to_date"/>
|
||||
<label name="product_type"/>
|
||||
<field name="product_type"/>
|
||||
|
||||
<label name="location"/>
|
||||
<field name="location" colspan="3"/>
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
</form>
|
||||
@@ -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" colspan="3"/>
|
||||
|
||||
<label name="output_quantity"/>
|
||||
<field name="output_quantity" symbol="unit"/>
|
||||
<label name="average_quantity"/>
|
||||
<field name="average_quantity" symbol="unit"/>
|
||||
|
||||
<label name="turnover"/>
|
||||
<field name="turnover"/>
|
||||
</form>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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. -->
|
||||
<tree>
|
||||
<field name="product" expand="2"/>
|
||||
<field name="output_quantity" symbol="unit" optional="0"/>
|
||||
<field name="average_quantity" symbol="unit" optional="0"/>
|
||||
<field name="turnover"/>
|
||||
</tree>
|
||||
Reference in New Issue
Block a user