[MIG] stock_account: new API

This commit is contained in:
Divyesh Makwana
2016-07-12 17:53:07 +02:00
committed by Thibault Delavallée
parent 288e0acca1
commit 1919945770
19 changed files with 501 additions and 658 deletions
-1
View File
@@ -40,7 +40,6 @@ Dashboard / Reports for Warehouse Management includes:
'data/stock_account_data_post_install.yml',
],
'test': [
],
'installable': True,
'auto_install': True,
+2 -4
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="property_stock_account_output_prd" model="ir.property">
<field name="name">property_stock_account_output</field>
<field name="fields_id" search="[('model','=','product.template'),('name','=','property_stock_account_output')]"/>
@@ -26,6 +25,5 @@
<field eval="False" name="value"/>
<field name="company_id" ref="base.main_company"/>
</record>
</data>
</openerp>
</odoo>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="default_cost_method" model="ir.property">
<field name="name">Cost Method Property</field>
<field name="fields_id" ref="field_product_template_property_cost_method"/>
@@ -27,6 +26,5 @@
<field name="value">manual_periodic</field>
<field name="type">selection</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.tools.translate import _
from openerp import api, models
from odoo import api, models, _
import logging
_logger = logging.getLogger(__name__)
@@ -15,11 +13,11 @@ class AccountChartTemplate(models.Model):
@api.model
def generate_journals(self, acc_template_ref, company, journals_dict=None):
journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}]
super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add)
return super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add)
@api.multi
def generate_properties(self, acc_template_ref, company, property_list=None):
super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company)
res = super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company)
PropertyObj = self.env['ir.property'] # Property Stock Journal
value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1)
if value:
@@ -32,10 +30,10 @@ class AccountChartTemplate(models.Model):
}
properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)])
if properties:
#the property exist: modify it
# the property exist: modify it
properties.write(vals)
else:
#create the property
# create the property
PropertyObj.create(vals)
todo_list = [ # Property Stock Accounts
@@ -56,10 +54,10 @@ class AccountChartTemplate(models.Model):
}
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
if properties:
#the property exist: modify it
# the property exist: modify it
properties.write(vals)
else:
#create the property
# create the property
PropertyObj.create(vals)
return True
return res
+17 -21
View File
@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api
from openerp.osv import osv
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
class account_invoice(osv.osv):
class AccountInvoice(models.Model):
_inherit = "account.invoice"
@api.model
def invoice_line_move_line_get(self):
res = super(account_invoice,self).invoice_line_move_line_get()
if self.company_id.anglo_saxon_accounting:
if self.type in ('out_invoice','out_refund'):
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_sale_move_lines(i_line))
res = super(AccountInvoice, self).invoice_line_move_line_get()
if self.company_id.anglo_saxon_accounting and self.type in ('out_invoice', 'out_refund'):
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_sale_move_lines(i_line))
return res
@api.model
@@ -29,9 +27,9 @@ class account_invoice(osv.osv):
res: The move line entries produced so far by the parent move_line_get.
"""
inv = i_line.invoice_id
company_currency = inv.company_id.currency_id.id
company_currency = inv.company_id.currency_id
if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time':
if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time':
fpos = i_line.invoice_id.fiscal_position_id
accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
# debit account dacc will be the output account
@@ -42,7 +40,7 @@ class account_invoice(osv.osv):
price_unit = i_line._get_anglo_saxon_price_unit()
return [
{
'type':'src',
'type': 'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
@@ -54,7 +52,7 @@ class account_invoice(osv.osv):
},
{
'type':'src',
'type': 'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
@@ -68,24 +66,22 @@ class account_invoice(osv.osv):
return []
class account_invoice_line(osv.osv):
class AccountInvoiceLine(models.Model):
_inherit = "account.invoice.line"
def _get_anglo_saxon_price_unit(self):
self.ensure_one()
return self.product_id.standard_price
def _get_price(self, cr, uid, ids, company_currency, price_unit, context=None):
line = self.browse(cr, uid, ids, context=context)[0]
cur_obj = self.pool.get('res.currency')
if line.invoice_id.currency_id.id != company_currency:
price = cur_obj.compute(cr, uid, company_currency, line.invoice_id.currency_id.id, price_unit * line.quantity, context={'date': line.invoice_id.date_invoice})
def _get_price(self, company_currency, price_unit):
if self.invoice_id.currency_id.id != company_currency:
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id)
else:
price = price_unit * line.quantity
return round(price, line.invoice_id.currency_id.decimal_places)
price = price_unit * self.quantity
return round(price, self.invoice_id.currency_id.decimal_places)
def get_invoice_line_account(self, type, product, fpos, company):
if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product':
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
return accounts['stock_input']
return super(account_invoice_line, self).get_invoice_line_account(type, product, fpos, company)
return super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
+124 -159
View File
@@ -1,79 +1,59 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp import api
from openerp.exceptions import UserError
from odoo import api, fields, models, tools, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
class product_template(osv.osv):
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = 'product.template'
def _get_cost_method(self, cr, uid, ids, field, args, context=None):
res = {}
for product in self.browse(cr, uid, ids, context=context):
if product.property_cost_method:
res[product.id] = product.property_cost_method
else:
res[product.id] = product.categ_id.property_cost_method
return res
property_valuation = fields.Selection([
('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
company_dependent=True, copy=True, default='manual_periodic',
help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \
"The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products.")
valuation = fields.Char(compute='_compute_valuation_type', inverse='_set_valuation_type')
property_cost_method = fields.Selection([
('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')], string='Costing Method',
company_dependent=True, copy=True,
help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year).
Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation.
Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""")
cost_method = fields.Char(compute='_compute_cost_method', inverse='_set_cost_method')
property_stock_account_input = fields.Many2one(
'account.account', 'Stock Input Account',
company_dependent=True, domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used.")
property_stock_account_output = fields.Many2one(
'account.account', 'Stock Output Account',
company_dependent=True, domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used.")
def _get_valuation_type(self, cr, uid, ids, field, args, context=None):
res = {}
for product in self.browse(cr, uid, ids, context=context):
if product.property_valuation:
res[product.id] = product.property_valuation
else:
res[product.id] = product.categ_id.property_valuation
return res
@api.one
@api.depends('property_valuation', 'categ_id.property_valuation')
def _compute_valuation_type(self):
self.valuation = self.property_valuation if self.property_valuation else self.categ_id.property_valuation
def _set_cost_method(self, cr, uid, ids, name, value, arg, context=None):
return self.write(cr, uid, ids, {'property_cost_method': value}, context=context)
@api.one
def _set_valuation_type(self):
return self.write({'property_valuation': self.valuation})
def _set_valuation_type(self, cr, uid, ids, name, value, arg, context=None):
return self.write(cr, uid, ids, {'property_valuation': value}, context=context)
@api.one
@api.depends('property_cost_method', 'categ_id.property_cost_method')
def _compute_cost_method(self):
self.cost_method = self.property_cost_method if self.property_cost_method else self.categ_id.property_cost_method
_columns = {
'property_valuation': fields.property(type='selection', selection=[('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \
"The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products."
, copy=True),
'valuation': fields.function(_get_valuation_type, fnct_inv=_set_valuation_type, type='char'), # TDE FIXME: store it ?
'property_cost_method': fields.property(type='selection', selection=[('standard', 'Standard Price'), ('average', 'Average Price'), ('real', 'Real Price')],
help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year).
Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation.
Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""",
string="Costing Method", copy=True),
'cost_method': fields.function(_get_cost_method, fnct_inv=_set_cost_method, type='char'), # TDE FIXME: store it ?
'property_stock_account_input': fields.property(
type='many2one',
relation='account.account',
string='Stock Input Account',
domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used."),
'property_stock_account_output': fields.property(
type='many2one',
relation='account.account',
string='Stock Output Account',
domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used."),
}
_defaults = {
'property_valuation': 'manual_periodic',
}
def create(self, cr, uid, vals, context=None):
if vals.get('cost_method'):
vals['property_cost_method'] = vals.pop('cost_method')
if vals.get('valuation'):
vals['property_valuation'] = vals.pop('valuation')
return super(product_template, self).create(cr, uid, vals, context=context)
@api.one
def _set_cost_method(self):
return self.write({'property_cost_method': self.cost_method})
@api.onchange('type')
def onchange_type_valuation(self):
@@ -86,7 +66,7 @@ class product_template(osv.osv):
""" Add the stock accounts related to product to the result of super()
@return: dictionary which contains information regarding stock accounts and super (income+expense accounts)
"""
accounts = super(product_template, self)._get_product_accounts()
accounts = super(ProductTemplate, self)._get_product_accounts()
res = self._get_asset_accounts()
accounts.update({
'stock_input': res['stock_input'] or self.property_stock_account_input or self.categ_id.property_stock_account_input_categ_id,
@@ -100,12 +80,12 @@ class product_template(osv.osv):
""" Add the stock journal related to product to the result of super()
@return: dictionary which contains all needed information regarding stock accounts and journal and super (income+expense accounts)
"""
accounts = super(product_template, self).get_product_accounts(fiscal_pos=fiscal_pos)
accounts = super(ProductTemplate, self).get_product_accounts(fiscal_pos=fiscal_pos)
accounts.update({'stock_journal': self.categ_id.property_stock_journal or False})
return accounts
class product_product(osv.osv):
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.onchange('type')
@@ -114,109 +94,94 @@ class product_product(osv.osv):
self.valuation = 'manual_periodic'
return {}
def do_change_standard_price(self, cr, uid, ids, new_price, account_id, context=None):
@api.multi
def do_change_standard_price(self, new_price, account_id):
""" Changes the Standard Price of Product and creates an account move accordingly."""
location_obj = self.pool.get('stock.location')
move_obj = self.pool.get('account.move')
if context is None:
context = {}
user_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
loc_ids = location_obj.search(cr, uid, [('usage', '=', 'internal'), ('company_id', '=', user_company_id)])
for rec_id in ids:
for location in location_obj.browse(cr, uid, loc_ids, context=context):
c = context.copy()
c.update({'location': location.id, 'compute_child': False})
product = self.browse(cr, uid, rec_id, context=c)
datas = self.pool['product.template'].get_product_accounts(cr, uid, product.product_tmpl_id.id, context=context)
AccountMove = self.env['account.move']
locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id)])
product_accounts = {(product.id, product.product_tmpl_id.get_product_accounts()) for product in self}
price_precision = dp.get_precision('Product Price')
for location in locations:
for product in self.with_context(location=location.id, compute_child=False):
diff = product.standard_price - new_price
if not diff:
if tools.float_is_zero(diff, precision_digits=price_precision):
raise UserError(_("No difference between standard price and new price!"))
qty = product.qty_available
if qty:
qty_available = product.qty_available
if qty_available:
# Accounting Entries
amount_diff = abs(diff * qty)
if diff * qty > 0:
if diff * qty_available > 0:
debit_account_id = account_id
credit_account_id = datas['stock_valuation'].id
credit_account_id = product_accounts[product.id]['stock_valuation'].id
else:
debit_account_id = datas['stock_valuation'].id
debit_account_id = product_accounts[product.id]['stock_valuation'].id
credit_account_id = account_id
lines = [(0, 0, {'name': _('Standard Price changed'),
'account_id': debit_account_id,
'debit': amount_diff,
'credit': 0,
}),
(0, 0, {
'name': _('Standard Price changed'),
'account_id': credit_account_id,
'debit': 0,
'credit': amount_diff,
})]
move_vals = {
'journal_id': datas['stock_journal'].id,
'journal_id': product_accounts[product.id]['stock_journal'].id,
'company_id': location.company_id.id,
'line_ids': lines,
'line_ids': [(0, 0, {
'name': _('Standard Price changed'),
'account_id': debit_account_id,
'debit': abs(diff * qty_available),
'credit': 0,
}), (0, 0, {
'name': _('Standard Price changed'),
'account_id': credit_account_id,
'debit': 0,
'credit': abs(diff * qty_available),
})],
}
move_id = move_obj.create(cr, uid, move_vals, context=context)
move_obj.post(cr, uid, [move_id], context=context)
self.write(cr, uid, rec_id, {'standard_price': new_price})
move = AccountMove.create(move_vals)
move.post()
self.write({'standard_price': new_price})
return True
class product_category(osv.osv):
class ProductCategory(models.Model):
_inherit = 'product.category'
_columns = {
'property_valuation': fields.property(
type='selection',
selection=[('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')],
string='Inventory Valuation',
required=True, copy=True,
help="If perpetual valuation is enabled for a product, the system "
"will automatically create journal entries corresponding to "
"stock moves, with product price as specified by the 'Costing "
"Method'. The inventory variation account set on the product "
"category will represent the current inventory value, and the "
"stock input and stock output account will hold the counterpart "
"moves for incoming and outgoing products."),
'property_cost_method': fields.property(
type='selection',
selection=[('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')],
string="Costing Method",
required=True, copy=True,
help="Standard Price: The cost price is manually updated at the end "
"of a specific period (usually once a year).\nAverage Price: "
"The cost price is recomputed at each incoming shipment and "
"used for the product valuation.\nReal Price: The cost price "
"displayed is the price of the last outgoing product (will be "
"used in case of inventory loss for example)."""),
'property_stock_journal': fields.property(
relation='account.journal',
type='many2one',
string='Stock Journal',
help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
'property_stock_account_input_categ_id': fields.property(
type='many2one',
relation='account.account',
string='Stock Input Account',
domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ",
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
"can also directly be set on each product"),
'property_stock_account_output_categ_id': fields.property(
type='many2one',
relation='account.account',
domain=[('deprecated', '=', False)],
string='Stock Output Account', oldname="property_stock_account_output_categ",
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
"can also directly be set on each product"),
'property_stock_valuation_account_id': fields.property(
type='many2one',
relation='account.account',
string="Stock Valuation Account",
domain=[('deprecated', '=', False)],
help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
}
property_valuation = fields.Selection([
('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
company_dependent=True, copy=True, required=True,
help="If perpetual valuation is enabled for a product, the system "
"will automatically create journal entries corresponding to "
"stock moves, with product price as specified by the 'Costing "
"Method'. The inventory variation account set on the product "
"category will represent the current inventory value, and the "
"stock input and stock output account will hold the counterpart "
"moves for incoming and outgoing products.")
property_cost_method = fields.Selection([
('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')], string="Costing Method",
company_dependent=True, copy=True, required=True,
help="Standard Price: The cost price is manually updated at the end "
"of a specific period (usually once a year).\nAverage Price: "
"The cost price is recomputed at each incoming shipment and "
"used for the product valuation.\nReal Price: The cost price "
"displayed is the price of the last outgoing product (will be "
"used in case of inventory loss for example).""")
property_stock_journal = fields.Many2one(
'account.journal', 'Stock Journal', company_dependent=True,
help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed.")
property_stock_account_input_categ_id = fields.Many2one(
'account.account', 'Stock Input Account', company_dependent=True,
domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ",
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
"can also directly be set on each product")
property_stock_account_output_categ_id = fields.Many2one(
'account.account', 'Stock Output Account', company_dependent=True,
domain=[('deprecated', '=', False)], oldname="property_stock_account_output_categ",
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
"can also directly be set on each product")
property_stock_valuation_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account', company_dependent=True,
domain=[('deprecated', '=', False)],
help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",)
+219 -267
View File
@@ -1,57 +1,56 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools import float_compare, float_round
from openerp.tools.translate import _
from openerp import SUPERUSER_ID, api, models
from openerp.exceptions import UserError
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_round
import logging
_logger = logging.getLogger(__name__)
class stock_inventory(osv.osv):
class StockInventory(models.Model):
_inherit = "stock.inventory"
_columns = {
'accounting_date': fields.date('Force Accounting Date', help="Choose the accounting date at which you want to value the stock moves created by the inventory instead of the default one (the inventory end date)"),
}
def post_inventory(self, cr, uid, ids, context=None):
if context is None:
context = {}
ctx = context.copy()
inv = self.browse(cr, uid, ids, context=context)[0]
if inv.accounting_date:
ctx['force_period_date'] = inv.accounting_date
return super(stock_inventory, self).post_inventory(cr, uid, ids, context=ctx)
accounting_date = fields.Date(
'Force Accounting Date',
help="Choose the accounting date at which you want to value the stock "
"moves created by the inventory instead of the default one (the "
"inventory end date)")
@api.multi
def post_inventory(self):
acc_inventories = self.filtered(lambda inventory: inventory.accounting_date)
for inventory in acc_inventories:
res = super(StockInventory, inventory.with_context(force_period_date=inventory.accounting_date)).post_inventory()
other_inventories = self - acc_inventories
if other_inventories:
res = super(StockInventory, other_inventories).post_inventory()
return res
#----------------------------------------------------------
# Stock Location
#----------------------------------------------------------
class stock_location(osv.osv):
class StockLocation(models.Model):
_inherit = "stock.location"
_columns = {
'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved from an internal location "
"into this location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations."),
'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved out of this location "
"and into an internal location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations."),
}
valuation_in_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account (Incoming)',
domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved from an internal location "
"into this location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations.")
valuation_out_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account (Outgoing)',
domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved out of this location "
"and into an internal location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations.")
#----------------------------------------------------------
# Quants
#----------------------------------------------------------
class stock_quant(osv.osv):
class StockQuant(models.Model):
_inherit = "stock.quant"
@api.multi
@@ -59,90 +58,82 @@ class stock_quant(osv.osv):
real_value_quants = self.filtered(lambda quant: quant.product_id.cost_method == 'real')
for quant in real_value_quants:
quant.inventory_value = quant.cost * quant.qty
other = self - real_value_quants
return super(stock_quant, other)._compute_inventory_value()
return super(StockQuant, self - real_value_quants)._compute_inventory_value()
@api.cr_uid_ids_context
def _price_update(self, cr, uid, quant_ids, newprice, context=None):
''' This function is called at the end of negative quant reconciliation and does the accounting entries adjustemnts and the update of the product cost price if needed
'''
if context is None:
context = {}
account_move_obj = self.pool['account.move']
super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
for quant in self.browse(cr, uid, quant_ids, context=context):
@api.multi
def _price_update(self, newprice):
''' This function is called at the end of negative quant reconciliation
and does the accounting entries adjustemnts and the update of the product
cost price if needed '''
super(StockQuant, self)._price_update(newprice)
for quant in self:
move = quant._get_latest_move()
valuation_update = newprice - quant.cost
# this is where we post accounting entries for adjustment, if needed
if not quant.company_id.currency_id.is_zero(valuation_update):
# If neg quant period already closed (likely with manual valuation), skip update
if account_move_obj._check_lock_date(cr, uid, [move.id], context=context):
ctx = dict(context, force_valuation_amount=valuation_update)
self._account_entry_move(cr, uid, [quant], move, context=ctx)
# If neg quant period already closed (likely with manual valuation), skip update
if not quant.company_id.currency_id.is_zero(valuation_update) and move._check_lock_date():
quant.with_context(force_valuation_amount=valuation_update)._account_entry_move(move)
#update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
#1) the product cost's method is 'real'
#2) we just fixed a negative quant caused by an outgoing shipment
# update the standard price of the product, only if we would have
# done it if we'd have had enough stock at first, which means
# 1) the product cost's method is 'real'
# 2) we just fixed a negative quant caused by an outgoing shipment
if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal':
self.pool.get('stock.move')._store_average_cost_price(cr, uid, [move.id], context=context)
move._store_average_cost_price()
def _account_entry_move(self, cr, uid, ids, move, context=None):
"""
Accounting Valuation Entries
def _account_entry_move(self, move):
""" Accounting Valuation Entries """
if move.product_id.type != 'product' or move.product_id.valuation != 'real_time':
# no stock valuation for consumable products
return False
if any(quant.owner_id or quant.qty <= 0 for quant in self):
# if the quant isn't owned by the company, we don't make any valuation en
# we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
# At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
# to make the adjustments when we know the real cost price.
return False
quants: browse record list of Quants to create accounting valuation entries for. Unempty and all quants are supposed to have the same location id (thay already moved in)
move: Move to use. browse record
"""
quants = self.browse(cr, uid, ids, context=context)
if context is None:
context = {}
location_obj = self.pool.get('stock.location')
location_from = move.location_id
location_to = quants[0].location_id
company_from = location_from and (location_from.usage == 'internal') and location_from.company_id or False
location_to = self[0].location_id # TDE FIXME: as the accounting is based on this value, should probably check all location_to to be the same
company_from = location_from.usage == 'internal' and location_from.company_id or False
company_to = location_to and (location_to.usage == 'internal') and location_to.company_id or False
if move.product_id.valuation != 'real_time':
return False
if move.product_id.type != 'product':
#No stock valuation for consumable products
return False
for q in quants:
if q.owner_id:
#if the quant isn't owned by the company, we don't make any valuation entry
return False
if q.qty <= 0:
#we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
#At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
#to make the adjustments when we know the real cost price.
return False
#in case of routes making the link between several warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
# Create Journal Entry for products arriving in the company
# Create Journal Entry for products arriving in the company; in case of routes making the link between several
# warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to):
ctx = context.copy()
ctx['force_company'] = company_to.id
journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx)
if location_from and location_from.usage == 'customer':
#goods returned from customer
self._create_account_move_line(cr, uid, quants.ids, move, acc_dest, acc_valuation, journal_id, context=ctx)
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
if location_from and location_from.usage == 'customer': # goods returned from customer
self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_dest, acc_valuation, journal_id)
else:
self._create_account_move_line(cr, uid, quants.ids, move, acc_src, acc_valuation, journal_id, context=ctx)
self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_src, acc_valuation, journal_id)
# Create Journal Entry for products leaving the company
if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to):
ctx = context.copy()
ctx['force_company'] = company_from.id
journal_id, acc_src, acc_dest, acc_valuation = self.pool['stock.move']._get_accounting_data_for_valuation(cr, uid, [move.id], context=ctx)
if location_to and location_to.usage == 'supplier':
#goods returned to supplier
self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_src, journal_id, context=ctx)
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
if location_to and location_to.usage == 'supplier': # goods returned to supplier
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_src, journal_id)
else:
self._create_account_move_line(cr, uid, quants.ids, move, acc_valuation, acc_dest, journal_id, context=ctx)
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id)
def _quant_create_from_move(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False, context=None):
quant_obj = self.pool.get('stock.quant')
quant = super(stock_quant, self)._quant_create_from_move(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to, context=context)
def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id):
# group quants by cost
quant_cost_qty = defaultdict(lambda: 0.0)
for quant in self:
quant_cost_qty[quant.cost] += quant.qty
AccountMove = self.env['account.move']
for cost, qty in quant_cost_qty.iteritems():
move_lines = move._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id)
date = self._context.get('force_period_date', fields.Date.context_today(self))
new_account_move = AccountMove.create({
'journal_id': journal_id,
'line_ids': move_lines,
'date': date,
'ref': move.picking_id.name})
new_account_move.post()
def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False):
quant = super(StockQuant, self)._quant_create_from_move(qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to)
quant._account_entry_move(move)
if move.product_id.valuation == 'real_time':
# If the precision required for the variable quant cost is larger than the accounting
@@ -161,217 +152,178 @@ class stock_quant(osv.osv):
if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\
and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0:
qty = quant.qty
cost = quant.cost
quant_correct = quant._quant_split(quant.qty - 1.0)
cost_correct += (qty * cost) - (qty * cost_rounded)
quant_obj.write(cr, SUPERUSER_ID, [quant.id], {'cost': cost_rounded}, context=context)
quant_obj.write(cr, SUPERUSER_ID, [quant_correct.id], {'cost': cost_correct}, context=context)
cost_correct += (quant.qty * quant.cost) - (quant.qty * cost_rounded)
quant.sudo().write({'cost': cost_rounded})
quant_correct.sudo().write({'cost': cost_correct})
return quant
@api.multi
def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
res = super(stock_quant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
res = super(StockQuant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
self._account_entry_move(move)
return res
def _create_account_move_line(self, cr, uid, ids, move, credit_account_id, debit_account_id, journal_id, context=None):
quants = self.browse(cr, uid, ids, context=context)
# group quants by cost
quant_cost_qty = {}
for quant in quants:
if quant_cost_qty.get(quant.cost):
quant_cost_qty[quant.cost] += quant.qty
else:
quant_cost_qty[quant.cost] = quant.qty
move_obj = self.pool.get('account.move')
for cost, qty in quant_cost_qty.items():
move_lines = self.pool['stock.move']._prepare_account_move_line(cr, uid, [move.id], qty, cost, credit_account_id, debit_account_id, context=context)
date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context))
new_move = move_obj.create(cr, uid, {'journal_id': journal_id,
'line_ids': move_lines,
'date': date,
'ref': move.picking_id.name}, context=context)
move_obj.post(cr, uid, [new_move], context=context)
class stock_move(osv.osv):
class StockMove(models.Model):
_inherit = "stock.move"
def action_done(self, cr, uid, ids, context=None):
self.product_price_update_before_done(cr, uid, ids, context=context)
res = super(stock_move, self).action_done(cr, uid, ids, context=context)
self.product_price_update_after_done(cr, uid, ids, context=context)
@api.multi
def action_done(self):
self.product_price_update_before_done()
res = super(StockMove, self).action_done()
self.product_price_update_after_done()
return res
def _store_average_cost_price(self, cr, uid, ids, context=None):
''' move is a browe record '''
move = self.browse(cr, uid, ids, context=context)[0]
product_obj = self.pool.get('product.product')
if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0:
#if there is a negative quant, the standard price shouldn't be updated
return
#Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will valuate it at 6€
average_valuation_price = 0.0
for q in move.quant_ids:
average_valuation_price += q.qty * q.cost
average_valuation_price = average_valuation_price / move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
ctx = dict(context or {}, force_company=move.company_id.id)
product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx)
self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context)
@api.multi
def product_price_update_before_done(self):
tmpl_dict = defaultdict(lambda: 0.0)
# adapt standard price on incomming moves if the product cost_method is 'average'
for move in self.filtered(lambda move: move.location_id.usage == 'supplier' and move.product_id.cost_method == 'average'):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
def product_price_update_before_done(self, cr, uid, ids, context=None):
product_obj = self.pool.get('product.product')
tmpl_dict = {}
for move in self.browse(cr, uid, ids, context=context):
#adapt standard price on incomming moves if the product cost_method is 'average'
if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
product = move.product_id
product_id = move.product_id.id
qty_available = move.product_id.qty_available
if tmpl_dict.get(product_id):
product_avail = qty_available + tmpl_dict[product_id]
else:
tmpl_dict[product_id] = 0
product_avail = qty_available
# if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use.
price_unit = move.get_price_unit()
if product_avail <= 0:
new_std_price = price_unit
else:
# Get the standard price
amount_unit = product.standard_price
new_std_price = ((amount_unit * product_avail) + (price_unit * move.product_qty)) / (product_avail + move.product_qty)
tmpl_dict[product_id] += move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
ctx = dict(context or {}, force_company=move.company_id.id)
product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx)
# if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use.
if product_tot_qty_available <= 0:
new_std_price = move.get_price_unit()
else:
# Get the standard price
amount_unit = move.product_id.standard_price
new_std_price = ((amount_unit * product_tot_qty_available) + (move.get_price_unit() * move.product_qty)) / (product_tot_qty_available + move.product_qty)
def product_price_update_after_done(self, cr, uid, ids, context=None):
'''
This method adapts the price on the product when necessary
'''
for move in self.browse(cr, uid, ids, context=context):
#adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
#or an inventory loss is made using the last value used for an outgoing valuation.
if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal':
#store the average price of the move on the move and product form
self._store_average_cost_price(cr, uid, [move.id], context=context)
tmpl_dict[move.product_id.id] += move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
move.product_id.with_context(force_company=move.company_id.id).write({'standard_price': new_std_price})
def _get_accounting_data_for_valuation(self, cr, uid, ids, context=None):
"""
Return the accounts and journal to use to post Journal Entries for the real-time
valuation of the quant.
@api.multi
def product_price_update_after_done(self):
''' Adapt standard price on outgoing moves if the product cost_method is 'real', so that a
return or an inventory loss is made using the last value used for an outgoing valuation. '''
to_update_moves = self.filtered(lambda move: move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal')
to_update_moves._store_average_cost_price()
:param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
:returns: journal_id, source account, destination account, valuation account
:raise: openerp.exceptions.UserError if any mandatory account or journal is not defined.
"""
move = self.browse(cr, uid, ids, context=context)[0]
product_obj = self.pool.get('product.template')
accounts = product_obj.browse(cr, uid, move.product_id.product_tmpl_id.id, context).get_product_accounts()
if move.location_id.valuation_out_account_id:
acc_src = move.location_id.valuation_out_account_id.id
def _store_average_cost_price(self):
""" Store the average price of the move on the move and product form """
for move in self:
# product_obj = self.pool.get('product.product')
if any(q.qty <= 0 for q in move.quant_ids) or move.product_qty == 0:
# if there is a negative quant, the standard price shouldn't be updated
return
# Note: here we can't store a quant.cost directly as we may have moved out 2 units
# (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't
# know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the
# average valuation price we are storing we will valuate it at 6€
valuation_price = sum(q.qty * q.cost for q in move.quant_ids)
average_valuation_price = valuation_price / move.product_qty
move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': average_valuation_price})
move.write({'price_unit': average_valuation_price})
@api.multi
def _get_accounting_data_for_valuation(self):
""" Return the accounts and journal to use to post Journal Entries for
the real-time valuation of the quant. """
self.ensure_one()
accounts_data = self.product_id.product_tmpl_id.get_product_accounts()
if self.location_id.valuation_out_account_id:
acc_src = self.location_id.valuation_out_account_id.id
else:
acc_src = accounts['stock_input'].id
acc_src = accounts_data['stock_input'].id
if move.location_dest_id.valuation_in_account_id:
acc_dest = move.location_dest_id.valuation_in_account_id.id
if self.location_dest_id.valuation_in_account_id:
acc_dest = self.location_dest_id.valuation_in_account_id.id
else:
acc_dest = accounts['stock_output'].id
acc_dest = accounts_data['stock_output'].id
acc_valuation = accounts.get('stock_valuation', False)
acc_valuation = accounts_data.get('stock_valuation', False)
if acc_valuation:
acc_valuation = acc_valuation.id
if not accounts.get('stock_journal', False):
if not accounts_data.get('stock_journal', False):
raise UserError(_('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts'))
if not acc_src:
raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name))
raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name))
if not acc_dest:
raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name))
raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name))
if not acc_valuation:
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
journal_id = accounts['stock_journal'].id
journal_id = accounts_data['stock_journal'].id
return journal_id, acc_src, acc_dest, acc_valuation
def _prepare_account_move_line(self, cr, uid, ids, qty, cost, credit_account_id, debit_account_id, context=None):
def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id):
"""
Generate the account.move.line values to post to track the stock valuation difference due to the
processing of the given quant.
"""
move = self.browse(cr, uid, ids, context=context)[0]
if context is None:
context = {}
currency_obj = self.pool.get('res.currency')
if context.get('force_valuation_amount'):
valuation_amount = context.get('force_valuation_amount')
self.ensure_one()
if self._context.get('force_valuation_amount'):
valuation_amount = self._context.get('force_valuation_amount')
else:
if move.product_id.cost_method == 'average':
valuation_amount = cost if move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price
if self.product_id.cost_method == 'average':
valuation_amount = cost if self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'internal' else self.product_id.standard_price
else:
valuation_amount = cost if move.product_id.cost_method == 'real' else move.product_id.standard_price
#the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
#the company currency... so we need to use round() before creating the accounting entries.
debit_value = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty)
#check that all data is correct
if move.company_id.currency_id.is_zero(debit_value):
raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,))
valuation_amount = cost if self.product_id.cost_method == 'real' else self.product_id.standard_price
# the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
# the company currency... so we need to use round() before creating the accounting entries.
debit_value = self.company_id.currency_id.round(valuation_amount * qty)
# check that all data is correct
if self.company_id.currency_id.is_zero(debit_value):
raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (self.product_id.name,))
credit_value = debit_value
if move.product_id.cost_method == 'average' and move.company_id.anglo_saxon_accounting:
#in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
#account books the real purchase price, while the stock account books the average price. The difference is
#booked in the dedicated price difference account.
if move.location_dest_id.usage == 'supplier' and move.origin_returned_move_id and move.origin_returned_move_id.purchase_line_id:
debit_value = move.origin_returned_move_id.price_unit * qty
#in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
#is made using the original average price to negate the delivery effect.
if move.location_id.usage == 'customer' and move.origin_returned_move_id:
debit_value = move.origin_returned_move_id.price_unit * qty
if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting:
# in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
# account books the real purchase price, while the stock account books the average price. The difference is
# booked in the dedicated price difference account.
if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id:
debit_value = self.origin_returned_move_id.price_unit * qty
# in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
# is made using the original average price to negate the delivery effect.
if self.location_id.usage == 'customer' and self.origin_returned_move_id:
debit_value = self.origin_returned_move_id.price_unit * qty
credit_value = debit_value
partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
partner_id = (self.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(self.picking_id.partner_id).id) or False
debit_line_vals = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'debit': debit_value,
'credit': 0,
'account_id': debit_account_id,
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'debit': debit_value,
'credit': 0,
'account_id': debit_account_id,
}
credit_line_vals = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'credit': credit_value,
'debit': 0,
'account_id': credit_account_id,
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'credit': credit_value,
'debit': 0,
'account_id': credit_account_id,
}
res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
if credit_value != debit_value:
#for supplier returns of product in average costing method, in anglo saxon mode
# for supplier returns of product in average costing method, in anglo saxon mode
diff_amount = debit_value - credit_value
price_diff_account = move.product_id.property_account_creditor_price_difference
price_diff_account = self.product_id.property_account_creditor_price_difference
if not price_diff_account:
price_diff_account = move.product_id.categ_id.property_account_creditor_price_difference_categ
price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ
if not price_diff_account:
raise UserError(_('Configuration error. Please configure the price difference account on the product or its category to process this operation.'))
price_diff_line = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'credit': diff_amount > 0 and diff_amount or 0,
'debit': diff_amount < 0 and -diff_amount or 0,
'account_id': price_diff_account.id,
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'credit': diff_amount > 0 and diff_amount or 0,
'debit': diff_amount < 0 and -diff_amount or 0,
'account_id': price_diff_account.id,
}
res.append((0, 0, price_diff_line))
return res
@@ -1,29 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from odoo import api, fields, models
class stock_config_settings(osv.osv_memory):
class StockConfigSettings(models.TransientModel):
_inherit = 'stock.config.settings'
_columns = {
'group_stock_inventory_valuation': fields.selection([
(0, "Periodic inventory valuation (recommended)"),
(1, 'Perpetual inventory valuation (stock move generates accounting entries)')
], "Inventory Valuation",
implied_group='stock_account.group_inventory_valuation',
help="""Allows to configure inventory valuations on products and product categories."""),
'module_stock_landed_costs': fields.selection([
(0, 'No landed costs'),
(1, 'Include landed costs in product costing computation')
], "Landed Costs",
help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products."""),
}
group_stock_inventory_valuation = fields.Selection([
(0, "Periodic inventory valuation (recommended)"),
(1, 'Perpetual inventory valuation (stock move generates accounting entries)')],
"Inventory Valuation", implied_group='stock_account.group_inventory_valuation',
help="""Allows to configure inventory valuations on products and product categories.""")
module_stock_landed_costs = fields.Selection([
(0, 'No landed costs'),
(1, 'Include landed costs in product costing computation')], "Landed Costs",
help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products.""")
def onchange_landed_costs(self, cr, uid, ids, module_landed_costs, context=None):
if module_landed_costs:
return {'value': {'group_stock_inventory_valuation': True}}
return {}
@api.onchange('module_stock_landed_costs')
def onchange_landed_costs(self):
if self.module_stock_landed_costs:
self.group_stock_inventory_valuation = 1
+52 -70
View File
@@ -1,92 +1,74 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
from odoo import api, fields, models, tools
class stock_history(osv.osv):
class StockHistory(models.Model):
_name = 'stock.history'
_auto = False
_order = 'date asc'
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
res = super(stock_history, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
if context is None:
context = {}
date = context.get('history_date', datetime.now())
move_id = fields.Many2one('stock.move', 'Stock Move', required=True)
location_id = fields.Many2one('stock.location', 'Location', required=True)
company_id = fields.Many2one('res.company', 'Company')
product_id = fields.Many2one('product.product', 'Product', required=True)
product_categ_id = fields.Many2one('product.category', 'Product Category', required=True)
quantity = fields.Float('Product Quantity')
date = fields.Datetime('Operation Date')
price_unit_on_quant = fields.Float('Value')
inventory_value = fields.Float('Inventory Value', compute='_compute_inventory_value', readonly=True)
source = fields.Char('Source')
product_template_id = fields.Many2one('product.template', 'Product Template', required=True)
serial_number = fields.Char('Lot/Serial Number', required=True)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
res = super(StockHistory, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
if 'inventory_value' in fields:
date = self._context.get('history_date', fields.Datetime.now())
stock_history = self.env['stock.history']
group_lines = {}
for line in res:
domain = line.get('__domain', domain)
group_lines.setdefault(str(domain), self.search(cr, uid, domain, context=context))
line_ids = set()
for ids in group_lines.values():
for product_id in ids:
line_ids.add(product_id)
line_ids = list(line_ids)
lines_rec = {}
if line_ids:
cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),))
lines_rec = cr.dictfetchall()
lines_dict = dict((line['id'], line) for line in lines_rec)
product_ids = list(set(line_rec['product_id'] for line_rec in lines_rec))
products_rec = self.pool['product.product'].read(cr, uid, product_ids, ['cost_method', 'id'], context=context)
products_dict = dict((product['id'], product) for product in products_rec)
cost_method_product_ids = list(set(product['id'] for product in products_rec if product['cost_method'] != 'real'))
histories = []
if cost_method_product_ids:
cr.execute('SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost FROM product_price_history WHERE product_id in %s AND datetime <= %s ORDER BY product_id, company_id, datetime DESC', (tuple(cost_method_product_ids), date))
histories = cr.dictfetchall()
group_lines.setdefault(str(domain), self.search(domain))
stock_history |= group_lines[str(domain)]
histories_dict = {}
for history in histories:
histories_dict[(history['product_id'], history['company_id'])] = history['cost']
not_real_cost_method_products = stock_history.mapped('product_id').filtered(lambda product: product.cost_method != 'real')
if not_real_cost_method_products:
self._cr.execute("""SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost
FROM product_price_history
WHERE product_id in %s AND datetime <= %s
ORDER BY product_id, company_id, datetime DESC""", (not_real_cost_method_products.ids, date))
for history in self._cr.dictfetchall():
histories_dict[(history['product_id'], history['company_id'])] = history['cost']
for line in res:
inv_value = 0.0
lines = group_lines.get(str(line.get('__domain', domain)))
for line_id in lines:
line_rec = lines_dict[line_id]
product = products_dict[line_rec['product_id']]
if product['cost_method'] == 'real':
price = line_rec['price_unit_on_quant']
for stock_history in group_lines.get(str(line.get('__domain', domain))):
product = stock_history.product_id
if product.cost_method == 'real':
price = stock_history.price_unit_on_quant
else:
price = histories_dict.get((product['id'], line_rec['company_id']), 0.0)
inv_value += price * line_rec['quantity']
price = histories_dict.get((product.id, stock_history.company_id.id), 0.0)
inv_value += price * stock_history.quantity
line['inventory_value'] = inv_value
return res
def _get_inventory_value(self, cr, uid, ids, name, attr, context=None):
if context is None:
context = {}
date = context.get('history_date')
product_obj = self.pool.get("product.product")
res = {}
for line in self.browse(cr, uid, ids, context=context):
if line.product_id.cost_method == 'real':
res[line.id] = line.quantity * line.price_unit_on_quant
else:
res[line.id] = line.quantity * product_obj.get_history_price(cr, uid, [line.product_id.id], line.company_id.id, date=date, context=context)
return res
@api.one
def _compute_inventory_value(self):
if self.product_id.cost_method == 'real':
self.inventory_value = self.quantity * self.price_unit_on_quant
else:
self.inventory_value = self.quantity * self.product_id.get_history_price(self.company_id.id, date=self._context.get('history_date', fields.Datetime.now()))
_columns = {
'move_id': fields.many2one('stock.move', 'Stock Move', required=True),
'location_id': fields.many2one('stock.location', 'Location', required=True),
'company_id': fields.many2one('res.company', 'Company'),
'product_id': fields.many2one('product.product', 'Product', required=True),
'product_categ_id': fields.many2one('product.category', 'Product Category', required=True),
'quantity': fields.float('Product Quantity'),
'date': fields.datetime('Operation Date'),
'price_unit_on_quant': fields.float('Value'),
'inventory_value': fields.function(_get_inventory_value, string="Inventory Value", type='float', readonly=True),
'source': fields.char('Source'),
'product_template_id': fields.many2one('product.template', 'Product Template', required=True),
'serial_number': fields.char('Lot/Serial Number', required=True),
}
def init(self, cr):
tools.drop_view_if_exists(cr, 'stock_history')
cr.execute("""
CREATE OR REPLACE VIEW stock_history AS (
@api.model_cr
def init(self):
tools.drop_view_if_exists(self._cr, 'stock_history')
self._cr.execute("""
CREATE VIEW stock_history AS (
SELECT MIN(id) as id,
move_id,
location_id,
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<record id="view_stock_history_report_tree" model="ir.ui.view">
<field name="name">Stock Value At Date</field>
<field name="model">stock.history</field>
@@ -60,6 +58,4 @@
</search>
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="0">
<record id="stock.group_stock_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_account_invoice'))]"/>
@@ -18,5 +18,5 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- Used in other modules for the tests, NOT in this one -->
@@ -18,4 +18,4 @@
</record>
</data>
</openerp>
</odoo>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_category_property_form" model="ir.ui.view">
@@ -99,4 +99,4 @@
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_inventory_form_inherit" model="ir.ui.view">
@@ -39,4 +39,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_stock_config_settings_inherit" model="ir.ui.view">
<field name="name">stock.config.settings.inherit</field>
@@ -8,11 +8,11 @@
<field name="arch" type="xml">
<xpath expr="//group[@name='traceability']" position="after">
<group string="Stock Accounting">
<field name="module_stock_landed_costs" widget="radio" on_change="onchange_landed_costs(module_stock_landed_costs)"/>
<field name="module_stock_landed_costs" widget="radio"/>
<field name="group_stock_inventory_valuation" widget="radio"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,71 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
class change_standard_price(osv.osv_memory):
class StockChangeStandardPrice(models.TransientModel):
_name = "stock.change.standard.price"
_description = "Change Standard Price"
_columns = {
'new_price': fields.float('Price', required=True, digits_compute=dp.get_precision('Product Price'),
help="If cost price is increased, stock variation account will be debited "
"and stock output account will be credited with the value = (difference of amount * quantity available).\n"
"If cost price is decreased, stock variation account will be creadited and stock input account will be debited."),
'counterpart_account_id': fields.many2one('account.account', string="Counter-Part Account", required=True, domain=[('deprecated', '=', False)]),
}
new_price = fields.Float(
'Price', digits_compute=dp.get_precision('Product Price'), required=True,
help="If cost price is increased, stock variation account will be debited "
"and stock output account will be credited with the value = (difference of amount * quantity available).\n"
"If cost price is decreased, stock variation account will be creadited and stock input account will be debited.")
counterpart_account_id = fields.Many2one(
'account.account', string="Counter-Part Account",
domain=[('deprecated', '=', False)], required=True)
def default_get(self, cr, uid, fields, context=None):
""" To get default values for the object.
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param fields: List of fields for which we want default values
@param context: A standard dictionary
@return: A dictionary which of fields with values.
"""
if context is None:
context = {}
if context.get("active_model") == 'product.product':
product_pool = self.pool.get('product.product')
else:
product_pool = self.pool.get('product.template')
product_obj = product_pool.browse(cr, uid, context.get('active_id', False))
@api.model
def default_get(self, fields):
res = super(StockChangeStandardPrice, self).default_get(fields)
res = super(change_standard_price, self).default_get(cr, uid, fields, context=context)
price = context.get('default_new_price') or product_obj.standard_price
if 'new_price' in fields:
res.update({'new_price': price})
if 'counterpart_account_id' in fields:
default_account = product_obj.property_account_expense_id or product_obj.categ_id.property_account_expense_categ_id
if default_account:
res.update({'counterpart_account_id': default_account.id})
product_or_template = self.env[self._context['active_model']].browse(self._context['active_id'])
if 'new_price' in fields and 'default_new_price' not in res:
res['new_price'] = product_or_template.standard_price
if 'counterpart_account_id' in fields and 'counterpart_account_id' not in res:
res['counterpart_account_id'] = product_or_template.property_account_expense_id.id or product_or_template.categ_id.property_account_expense_categ_id.id
return res
def change_price(self, cr, uid, ids, context=None):
""" Changes the Standard Price of Product.
And creates an account move accordingly.
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param ids: List of IDs selected
@param context: A standard dictionary
@return:
"""
if context is None:
context = {}
rec_id = context.get('active_id', False)
assert rec_id, _('Active ID is not set in Context.')
new_price = self.browse(cr, uid, ids, context=context)[0].new_price
if context.get("active_model") == 'product.template':
prod_obj = self.pool.get('product.template')
rec_ids = prod_obj.browse(cr, uid, rec_id, context=context).product_variant_ids.mapped('id')
@api.multi
def change_price(self):
""" Changes the Standard Price of Product and creates an account move accordingly. """
self.ensure_one()
if self._context['active_model'] == 'product.template':
products = self.env['product.template'].browse(self._context['active_id']).product_variant_ids
else:
rec_ids = [rec_id]
res = self.browse(cr, uid, ids, context=context)[0]
self.pool.get('product.product').do_change_standard_price(cr, uid, rec_ids, new_price, res.counterpart_account_id.id, context=context)
products = self.env['product.product'].browse(self._context['active_id'])
products.do_change_standard_price(self.new_price, self.counterpart_account_id.id)
return {'type': 'ir.actions.act_window_close'}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_change_standard_price" model="ir.ui.view">
<field name="name">Change Standard Price</field>
@@ -34,5 +34,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,31 +1,26 @@
from openerp.osv import fields, osv
from openerp.tools.translate import _
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
class wizard_valuation_history(osv.osv_memory):
class WizardValuationHistory(models.TransientModel):
_name = 'wizard.valuation.history'
_description = 'Wizard that opens the stock valuation history table'
_columns = {
'choose_date': fields.boolean('Inventory at Date'),
'date': fields.datetime('Date', required=True),
}
_defaults = {
'choose_date': False,
'date': fields.datetime.now,
}
choose_date = fields.Boolean('Inventory at Date')
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
def open_table(self, cr, uid, ids, context=None):
if context is None:
context = {}
data = self.read(cr, uid, ids, context=context)[0]
ctx = context.copy()
ctx['history_date'] = data['date']
ctx['search_default_group_by_product'] = True
ctx['search_default_group_by_location'] = True
@api.multi
def open_table(self):
self.ensure_one()
ctx = dict(
self._context,
history_date=self.date,
search_default_group_by_product=True,
search_default_group_by_location=True)
return {
'domain': "[('date', '<=', '" + data['date'] + "')]",
'domain': "[('date', '<=', '" + self.date + "')]",
'name': _('Stock Value At Date'),
'view_type': 'form',
'view_mode': 'tree',
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<record id="view_wizard_valuation_history" model="ir.ui.view">
<field name="name">Inventory at Date</field>
<field name="model">wizard.valuation.history</field>
@@ -31,4 +31,4 @@
<field name="target">new</field>
</record>
<menuitem id="menu_action_wizard_valuation_history" action="action_wizard_stock_valuation_history" parent="stock.menu_warehouse_report" sequence="1" groups="base.group_no_one"/>
</openerp>
</odoo>