mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit of stock_landed_costs_average for 11.0
This commit is contained in:
1
stock_landed_costs_average/__init__.py
Normal file
1
stock_landed_costs_average/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
21
stock_landed_costs_average/__manifest__.py
Normal file
21
stock_landed_costs_average/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
'name': 'Landed Costs Average',
|
||||||
|
'summary': 'Use Landed Costs on Average Cost inventory.',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "Hibou Corp. <hello@hibou.io>",
|
||||||
|
'category': 'Warehouse',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'complexity': 'expert',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'stock_landed_costs',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
1
stock_landed_costs_average/models/__init__.py
Normal file
1
stock_landed_costs_average/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import stock_landed_cost
|
||||||
96
stock_landed_costs_average/models/stock_landed_cost.py
Normal file
96
stock_landed_costs_average/models/stock_landed_cost.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LandedCost(models.Model):
|
||||||
|
_inherit = 'stock.landed.cost'
|
||||||
|
|
||||||
|
def get_valuation_lines(self):
|
||||||
|
"""
|
||||||
|
Override for allowing Average value inventory.
|
||||||
|
:return: list of new line values
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for move in self.mapped('picking_ids').mapped('move_lines'):
|
||||||
|
# Only allow for real time valuated products with 'average' or 'fifo' cost
|
||||||
|
if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average'):
|
||||||
|
continue
|
||||||
|
vals = {
|
||||||
|
'product_id': move.product_id.id,
|
||||||
|
'move_id': move.id,
|
||||||
|
'quantity': move.product_qty,
|
||||||
|
'former_cost': move.value,
|
||||||
|
'weight': move.product_id.weight * move.product_qty,
|
||||||
|
'volume': move.product_id.volume * move.product_qty
|
||||||
|
}
|
||||||
|
lines.append(vals)
|
||||||
|
|
||||||
|
if not lines and self.mapped('picking_ids'):
|
||||||
|
raise UserError(_('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def button_validate(self):
|
||||||
|
"""
|
||||||
|
Override to directly set new standard_price on product if average costed.
|
||||||
|
:return: True
|
||||||
|
"""
|
||||||
|
if any(cost.state != 'draft' for cost in self):
|
||||||
|
raise UserError(_('Only draft landed costs can be validated'))
|
||||||
|
if any(not cost.valuation_adjustment_lines for cost in self):
|
||||||
|
raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.'))
|
||||||
|
if not self._check_sum():
|
||||||
|
raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))
|
||||||
|
|
||||||
|
for cost in self:
|
||||||
|
move = self.env['account.move']
|
||||||
|
move_vals = {
|
||||||
|
'journal_id': cost.account_journal_id.id,
|
||||||
|
'date': cost.date,
|
||||||
|
'ref': cost.name,
|
||||||
|
'line_ids': [],
|
||||||
|
}
|
||||||
|
for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
|
||||||
|
# Prorate the value at what's still in stock
|
||||||
|
_logger.warn('(line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost')
|
||||||
|
_logger.warn('(%s / %s) * %s' % (line.move_id.remaining_qty, line.move_id.product_qty, line.additional_landed_cost))
|
||||||
|
cost_to_add = (line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
|
||||||
|
_logger.warn('cost_to_add: ' + str(cost_to_add))
|
||||||
|
|
||||||
|
new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost
|
||||||
|
line.move_id.write({
|
||||||
|
'landed_cost_value': new_landed_cost_value,
|
||||||
|
'value': line.move_id.value + cost_to_add,
|
||||||
|
'remaining_value': line.move_id.remaining_value + cost_to_add,
|
||||||
|
'price_unit': (line.move_id.value + new_landed_cost_value) / line.move_id.product_qty,
|
||||||
|
})
|
||||||
|
# `remaining_qty` is negative if the move is out and delivered products that were not
|
||||||
|
# in stock.
|
||||||
|
qty_out = 0
|
||||||
|
if line.move_id._is_in():
|
||||||
|
qty_out = line.move_id.product_qty - line.move_id.remaining_qty
|
||||||
|
elif line.move_id._is_out():
|
||||||
|
qty_out = line.move_id.product_qty
|
||||||
|
move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)
|
||||||
|
|
||||||
|
# Need to set the standard price directly on the product.
|
||||||
|
if line.product_id.cost_method == 'average':
|
||||||
|
# From product.do_change_standard_price
|
||||||
|
quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', '=', line.product_id.id)],
|
||||||
|
['location_id'], ['location_id'])
|
||||||
|
quant_loc_ids = [loc['location_id'][0] for loc in quant_locs]
|
||||||
|
locations = self.env['stock.location'].search(
|
||||||
|
[('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id),
|
||||||
|
('id', 'in', quant_loc_ids)])
|
||||||
|
qty_available = line.product_id.with_context(location=locations.ids).qty_available
|
||||||
|
total_cost = (qty_available * line.product_id.standard_price) + cost_to_add
|
||||||
|
line.product_id.write({'standard_price': total_cost / qty_available})
|
||||||
|
|
||||||
|
move = move.create(move_vals)
|
||||||
|
cost.write({'state': 'done', 'account_move_id': move.id})
|
||||||
|
move.post()
|
||||||
|
return True
|
||||||
1
stock_landed_costs_average/tests/__init__.py
Normal file
1
stock_landed_costs_average/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_stock_landed_cost
|
||||||
102
stock_landed_costs_average/tests/test_stock_landed_cost.py
Normal file
102
stock_landed_costs_average/tests/test_stock_landed_cost.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from odoo.addons.stock_landed_costs.tests.test_stock_landed_costs_purchase import TestLandedCosts
|
||||||
|
|
||||||
|
|
||||||
|
class TestLandedCostsAverage(TestLandedCosts):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestLandedCostsAverage, self).setUp()
|
||||||
|
self.product_refrigerator.cost_method = 'average'
|
||||||
|
self.product_oven.cost_method = 'average'
|
||||||
|
|
||||||
|
def test_00_landed_costs_on_incoming_shipment(self):
|
||||||
|
original_standard_price = self.product_refrigerator.standard_price
|
||||||
|
super(TestLandedCostsAverage, self).test_00_landed_costs_on_incoming_shipment()
|
||||||
|
self.assertTrue(original_standard_price != self.product_refrigerator.standard_price)
|
||||||
|
|
||||||
|
def test_01_landed_costs_simple_average(self):
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 0.0)
|
||||||
|
picking_in = self.Picking.create({
|
||||||
|
'partner_id': self.supplier_id,
|
||||||
|
'picking_type_id': self.picking_type_in_id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 5,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_in.id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
picking_in.action_confirm()
|
||||||
|
res_dict = picking_in.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 5.0)
|
||||||
|
|
||||||
|
stock_landed_cost = self._create_landed_costs({
|
||||||
|
'equal_price_unit': 50,
|
||||||
|
'quantity_price_unit': 0,
|
||||||
|
'weight_price_unit': 0,
|
||||||
|
'volume_price_unit': 0}, picking_in)
|
||||||
|
stock_landed_cost.compute_landed_cost()
|
||||||
|
stock_landed_cost.button_validate()
|
||||||
|
account_entry = self.env['account.move.line'].read_group(
|
||||||
|
[('move_id', '=', stock_landed_cost.account_move_id.id)], ['debit', 'credit', 'move_id'], ['move_id'])[0]
|
||||||
|
self.assertEqual(account_entry['debit'], 50.0, 'Wrong Account Entry')
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 11.0)
|
||||||
|
|
||||||
|
def test_02_landed_costs_average(self):
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 0.0)
|
||||||
|
picking_in = self.Picking.create({
|
||||||
|
'partner_id': self.supplier_id,
|
||||||
|
'picking_type_id': self.picking_type_in_id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 5,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_in.id,
|
||||||
|
'location_id': self.supplier_location_id,
|
||||||
|
'location_dest_id': self.stock_location_id})
|
||||||
|
picking_in.action_confirm()
|
||||||
|
res_dict = picking_in.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 5.0)
|
||||||
|
|
||||||
|
picking_out = self.Picking.create({
|
||||||
|
'partner_id': self.customer_id,
|
||||||
|
'picking_type_id': self.picking_type_out_id,
|
||||||
|
'location_id': self.stock_location_id,
|
||||||
|
'location_dest_id': self.customer_location_id})
|
||||||
|
self.Move.create({
|
||||||
|
'name': self.product_refrigerator.name,
|
||||||
|
'product_id': self.product_refrigerator.id,
|
||||||
|
'product_uom_qty': 2,
|
||||||
|
'product_uom': self.product_refrigerator.uom_id.id,
|
||||||
|
'picking_id': picking_out.id,
|
||||||
|
'location_id': self.stock_location_id,
|
||||||
|
'location_dest_id': self.customer_location_id})
|
||||||
|
picking_out.action_confirm()
|
||||||
|
picking_out.action_assign()
|
||||||
|
res_dict = picking_out.button_validate()
|
||||||
|
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||||
|
wizard.process()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 1.0)
|
||||||
|
self.assertEqual(self.product_refrigerator.qty_available, 3.0)
|
||||||
|
|
||||||
|
stock_landed_cost = self._create_landed_costs({
|
||||||
|
'equal_price_unit': 50,
|
||||||
|
'quantity_price_unit': 0,
|
||||||
|
'weight_price_unit': 0,
|
||||||
|
'volume_price_unit': 0}, picking_in)
|
||||||
|
stock_landed_cost.compute_landed_cost()
|
||||||
|
stock_landed_cost.button_validate()
|
||||||
|
self.assertEqual(self.product_refrigerator.standard_price, 11.0)
|
||||||
Reference in New Issue
Block a user