From 991cc17d30b282519a9502661ecd165d8ffd791d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 16 Oct 2018 08:19:30 -0700 Subject: [PATCH] Initial commit of `stock_landed_costs_average` for 11.0 --- stock_landed_costs_average/__init__.py | 1 + stock_landed_costs_average/__manifest__.py | 21 ++++ stock_landed_costs_average/models/__init__.py | 1 + .../models/stock_landed_cost.py | 96 +++++++++++++++++ stock_landed_costs_average/tests/__init__.py | 1 + .../tests/test_stock_landed_cost.py | 102 ++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 stock_landed_costs_average/__init__.py create mode 100644 stock_landed_costs_average/__manifest__.py create mode 100644 stock_landed_costs_average/models/__init__.py create mode 100644 stock_landed_costs_average/models/stock_landed_cost.py create mode 100644 stock_landed_costs_average/tests/__init__.py create mode 100644 stock_landed_costs_average/tests/test_stock_landed_cost.py diff --git a/stock_landed_costs_average/__init__.py b/stock_landed_costs_average/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_landed_costs_average/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_landed_costs_average/__manifest__.py b/stock_landed_costs_average/__manifest__.py new file mode 100644 index 00000000..1c02704e --- /dev/null +++ b/stock_landed_costs_average/__manifest__.py @@ -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. ", + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'complexity': 'expert', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +""", + 'depends': [ + 'stock_landed_costs', + ], + 'demo': [], + 'data': [ + ], + 'auto_install': False, + 'installable': True, +} diff --git a/stock_landed_costs_average/models/__init__.py b/stock_landed_costs_average/models/__init__.py new file mode 100644 index 00000000..e9907b68 --- /dev/null +++ b/stock_landed_costs_average/models/__init__.py @@ -0,0 +1 @@ +from . import stock_landed_cost diff --git a/stock_landed_costs_average/models/stock_landed_cost.py b/stock_landed_costs_average/models/stock_landed_cost.py new file mode 100644 index 00000000..19c761f1 --- /dev/null +++ b/stock_landed_costs_average/models/stock_landed_cost.py @@ -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 \ No newline at end of file diff --git a/stock_landed_costs_average/tests/__init__.py b/stock_landed_costs_average/tests/__init__.py new file mode 100644 index 00000000..68fef37a --- /dev/null +++ b/stock_landed_costs_average/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_landed_cost diff --git a/stock_landed_costs_average/tests/test_stock_landed_cost.py b/stock_landed_costs_average/tests/test_stock_landed_cost.py new file mode 100644 index 00000000..240a7bf7 --- /dev/null +++ b/stock_landed_costs_average/tests/test_stock_landed_cost.py @@ -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)