Changes in the way we calcualte total_cost, add round_price to totals. (#2)

Tests.
Pyflakes.

Task #068419
This commit is contained in:
Juanjo Garcia Pagan 2023-05-16 15:11:56 +02:00 committed by Juanjo Garcia
parent 8988c3da2f
commit 4032aa6d54
8 changed files with 159 additions and 215 deletions

View File

@ -6,7 +6,6 @@ from . import stock
def register():
Pool.register(
stock.LotCostCategory,
stock.LotCostLine,
stock.Lot,
stock.Move,
module='stock_lot_cost', type_='model')

139
stock.py
View File

@ -1,10 +1,12 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from decimal import Decimal
import datetime
from trytond.model import ModelSQL, ModelView, Unique, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.modules.product import round_price
class LotCostCategory(ModelSQL, ModelView):
@ -23,85 +25,76 @@ class LotCostCategory(ModelSQL, ModelView):
]
class LotCostLine(ModelSQL, ModelView):
'''Stock Lot Cost Line'''
__name__ = 'stock.lot.cost_line'
lot = fields.Many2One('stock.lot', 'Lot', required=True,
ondelete='CASCADE')
category = fields.Many2One('stock.lot.cost_category', 'Category',
required=True)
unit_price = fields.Numeric('Unit Price', required=True)
origin = fields.Reference('Origin', selection='get_origin', readonly=True)
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
return [
'stock.move',
]
@classmethod
def get_origin(cls):
pool = Pool()
Model = pool.get('ir.model')
models = cls._get_origin()
models = Model.search([
('model', 'in', models),
])
return [('', '')] + [(m.model, m.name) for m in models]
class Lot(metaclass=PoolMeta):
__name__ = 'stock.lot'
cost_lines = fields.One2Many('stock.lot.cost_line', 'lot', 'Cost Lines')
cost_price = fields.Function(fields.Numeric('Cost Price'),
'get_cost_price')
cost_price = fields.Function(fields.Numeric("Cost Price"),
'get_lot_prices')
total_cost = fields.Function(fields.Numeric("Total Cost"),
'get_lot_prices')
def get_cost_price(self, name):
if not self.cost_lines:
return
return sum(l.unit_price for l in self.cost_lines if l.unit_price is not
None)
@fields.depends('product', 'cost_lines')
def on_change_product(self):
try:
super(Lot, self).on_change_product()
except AttributeError:
pass
if not self.id or self.id <= 0:
return
cost_lines = self._on_change_product_cost_lines()
if cost_lines:
cost_lines = cost_lines.get('add')
LotCostLine = Pool().get('stock.lot.cost_line')
lot_cost_lines = LotCostLine.search([
('lot', '=', self.id),
('category', '=', cost_lines[0][1]['category']),
('unit_price', '=', cost_lines[0][1]['unit_price']),
])
if lot_cost_lines:
self.cost_lines = lot_cost_lines
def _on_change_product_cost_lines(self):
@classmethod
def get_lot_prices(cls, lots, names):
pool = Pool()
ModelData = pool.get('ir.model.data')
Move = pool.get('stock.move')
Product = pool.get('product.product')
Location = pool.get('stock.location')
if not self.product:
return {}
res = {}
ids = [x.id for x in lots]
for name in ['cost_price', 'total_cost']:
res[name] = dict.fromkeys(ids)
category_id = ModelData.get_id('stock_lot_cost',
'cost_category_standard_price')
return {
'add': [(0, {
'category': category_id,
'unit_price': self.product.cost_price,
})],
}
warehouse_ids = [location.id for location in Location.search(
[('type', '=', 'warehouse')])]
product_ids = list(set(lot.product.id for lot in lots if lot.product))
lot_ids = list(set(lot.id for lot in lots))
moves = Move.search([
('lot', 'in', lot_ids),
('from_location.type', 'in', ['supplier', 'production']),
('to_location.type', '=', 'storage'),
('state', '=', 'done'),
])
with Transaction().set_context({'stock_date_end': datetime.date.max}):
pbl = Product.products_by_location(warehouse_ids,
with_childs=True,
grouping=('product', 'lot'),
grouping_filter=(product_ids, lot_ids))
lot_moves = {}
for move in moves:
if move.lot not in lot_moves:
lot_moves[move.lot] = []
lot_moves[move.lot].append(move)
for lot in lots:
res['total_cost'][lot.id] = Decimal(0)
res['cost_price'][lot.id] = Decimal(0)
if not lot in lot_moves:
continue
warehouse_quantity = Decimal(0)
for k, v in pbl.items():
key = k[1:]
if key == (lot.product.id, lot.id):
warehouse_quantity += Decimal(v)
total_price = Decimal(sum(Decimal(m.unit_price) * Decimal(
m.internal_quantity) for m in lot_moves[lot] if (
m.unit_price and m.internal_quantity)))
total_quantity = Decimal(
sum(m.internal_quantity for m in lot_moves[lot]))
res['cost_price'][lot.id] = round_price(total_price/total_quantity)
res['total_cost'][lot.id] = round_price(
total_price/total_quantity) * warehouse_quantity
for name in list(res.keys()):
if name not in names:
del res[name]
return res
class Move(metaclass=PoolMeta):

View File

@ -52,63 +52,6 @@
<field name="perm_delete" eval="True"/>
</record>
<!-- stock.lot.cost_line -->
<record model="ir.ui.view" id="stock_lot_cost_line_form_view">
<field name="model">stock.lot.cost_line</field>
<field name="type">form</field>
<field name="name">lot_cost_line_form</field>
</record>
<record model="ir.ui.view" id="stock_lot_cost_line_list_view">
<field name="model">stock.lot.cost_line</field>
<field name="type">tree</field>
<field name="name">lot_cost_line_list</field>
</record>
<record model="ir.action.act_window" id="act_stock_lot_cost_line">
<field name="name">Stock Lot Cost Line</field>
<field name="res_model">stock.lot.cost_line</field>
</record>
<record model="ir.action.act_window.view"
id="act_stock_lot_cost_line_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="stock_lot_cost_line_list_view"/>
<field name="act_window" ref="act_stock_lot_cost_line"/>
</record>
<record model="ir.action.act_window.view"
id="act_stock_lot_cost_line_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="stock_lot_cost_line_form_view"/>
<field name="act_window" ref="act_stock_lot_cost_line"/>
</record>
<record model="ir.model.access" id="access_stock_lot_cost_line_default">
<field name="model"
search="[('model', '=', 'stock.lot.cost_line')]"/>
<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.model.access" id="access_stock_lot_cost_line">
<field name="model"
search="[('model', '=', 'stock.lot.cost_line')]"/>
<field name="group" ref="stock.group_stock"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_stock_lot_cost_line_admin">
<field name="model"
search="[('model', '=', 'stock.lot.cost_line')]"/>
<field name="group" ref="stock.group_stock_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- stock.lot -->
<record model="ir.ui.view" id="lot_view_form">
<field name="model">stock.lot</field>

View File

@ -0,0 +1,89 @@
=======================
Stock Lot Cost Scenario
=======================
Imports::
>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from decimal import Decimal
>>> from proteus import config, Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import (
... create_company, get_company)
>>> today = datetime.date.today()
Activate modules::
>>> config = activate_modules('stock_lot_cost')
>>> Location = Model.get('stock.location')
>>> Lot = Model.get('stock.lot')
>>> Party = Model.get('party.party')
>>> ProductTemplate = Model.get('product.template')
>>> ProductUom = Model.get('product.uom')
>>> Move = Model.get('stock.move')
Create company::
>>> _ = create_company()
>>> company = get_company()
Create product::
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> template = ProductTemplate()
>>> template.name = 'Product'
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.list_price = Decimal('20')
>>> 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')])
>>> output_loc, = Location.find([('code', '=', 'OUT')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
Create lot::
>>> lot = Lot()
>>> lot.number = 'LOT'
>>> lot.product = product
>>> lot.save()
Create supplier moves move::
>>> move1_in = Move()
>>> move1_in.from_location = supplier_loc
>>> move1_in.to_location = storage_loc
>>> move1_in.product = product
>>> move1_in.company = company
>>> move1_in.lot = lot
>>> move1_in.quantity = 100
>>> move1_in.unit_price = Decimal('1')
>>> move1_in.save()
>>> move1_in.click('do')
>>> move2_in = Move()
>>> move2_in.from_location = supplier_loc
>>> move2_in.to_location = storage_loc
>>> move2_in.product = product
>>> move2_in.company = company
>>> move2_in.lot = lot
>>> move2_in.quantity = 100
>>> move2_in.unit_price = Decimal('2')
>>> move2_in.save()
>>> move2_in.click('do')
Check the lot costs::
>>> lot.cost_price
Decimal('1.5000')
>>> lot.total_cost
Decimal('300.0000')

View File

@ -1,74 +1,13 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.modules.company.tests import (CompanyTestMixin, create_company,
set_company)
from trytond.tests.test_tryton import ModuleTestCase
from trytond.modules.company.tests import CompanyTestMixin
class StockLotCostTestCase(CompanyTestMixin, ModuleTestCase):
'Test StockLotCost module'
module = 'stock_lot_cost'
@with_transaction()
def test0010lot_cost_price(self):
'Test Lot.cost_price'
pool = Pool()
Template = pool.get('product.template')
Product = pool.get('product.product')
Uom = pool.get('product.uom')
ModelData = pool.get('ir.model.data')
Lot = pool.get('stock.lot')
LotCostLine = pool.get('stock.lot.cost_line')
company = create_company()
with set_company(company):
kg, = Uom.search([('name', '=', 'Kilogram')])
g, = Uom.search([('name', '=', 'Gram')])
template, = Template.create([{
'name': 'Test Lot.cost_price',
'type': 'goods',
'list_price': Decimal(20),
'cost_price_method': 'fixed',
'default_uom': kg.id,
}])
product, = Product.create([{
'template': template.id,
'cost_price': Decimal(10),
}])
lot_cost_category_id = ModelData.get_id('stock_lot_cost',
'cost_category_standard_price')
lot = Lot(
number='1',
product=product.id
)
lot.save()
# Lot.product.on_change test
lot_vals = lot._on_change_product_cost_lines()
values = {}
for (k, v) in lot_vals.items():
vals = v[0][1]
values['cost_lines'] = [(k.replace('add', 'create'),
[vals])]
Lot.write([lot], values)
self.assertEqual(lot.cost_price, template.cost_price)
LotCostLine.create([{
'lot': lot.id,
'category': lot_cost_category_id,
'unit_price': Decimal(3),
}, {
'lot': lot.id,
'category': lot_cost_category_id,
'unit_price': Decimal(2),
}])
self.assertEqual(lot.cost_price, Decimal(15))
del ModuleTestCase

View File

@ -1,14 +0,0 @@
<?xml version="1.0"?>
<!--The COPYRIGHT file at the top level of this repository
contains the full copyright notices and license terms. -->
<form>
<label name="lot"/>
<field name="lot"/>
<label name="origin"/>
<field name="origin"/>
<newline/>
<label name="category"/>
<field name="category"/>
<label name="unit_price"/>
<field name="unit_price"/>
</form>

View File

@ -1,6 +0,0 @@
<tree>
<field name="lot"/>
<field name="category"/>
<field name="unit_price"/>
<field name="origin"/>
</tree>

View File

@ -5,6 +5,7 @@
<xpath expr="/form/field[@name='product']" position="after">
<label name="cost_price"/>
<field name="cost_price"/>
<field name="cost_lines" colspan="4"/>
<label name="total_cost"/>
<field name="total_cost"/>
</xpath>
</data>