Changes in the way we calcualte total_cost, add round_price to totals. (#2)
Tests. Pyflakes. Task #068419
This commit is contained in:
parent
8988c3da2f
commit
4032aa6d54
|
@ -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
139
stock.py
|
@ -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):
|
||||
|
|
57
stock.xml
57
stock.xml
|
@ -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>
|
||||
|
|
|
@ -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')
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -1,6 +0,0 @@
|
|||
<tree>
|
||||
<field name="lot"/>
|
||||
<field name="category"/>
|
||||
<field name="unit_price"/>
|
||||
<field name="origin"/>
|
||||
</tree>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue