From 67a37d87f357669d2ec038c9f0687e7c2c8cf63b Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Wed, 18 Sep 2024 12:24:58 -0300 Subject: [PATCH] [IMP] Add stock_currency_valuation module --- stock_currency_valuation/__init__.py | 2 + stock_currency_valuation/__manifest__.py | 26 ++++++ stock_currency_valuation/models/__init__.py | 5 ++ .../models/product_category.py | 12 +++ .../models/product_product.py | 18 ++++ .../models/product_template.py | 70 +++++++++++++++ stock_currency_valuation/models/stock_move.py | 87 +++++++++++++++++++ .../models/stock_valuation_layer.py | 35 ++++++++ stock_currency_valuation/views/product.xml | 18 ++++ .../views/product_category.xml | 13 +++ .../views/stock_valuation_layer.xml | 28 ++++++ stock_currency_valuation/wizard/__init__,py | 1 + .../stock_valuation_layer_revaluation.py | 22 +++++ 13 files changed, 337 insertions(+) create mode 100644 stock_currency_valuation/__init__.py create mode 100644 stock_currency_valuation/__manifest__.py create mode 100644 stock_currency_valuation/models/__init__.py create mode 100644 stock_currency_valuation/models/product_category.py create mode 100644 stock_currency_valuation/models/product_product.py create mode 100644 stock_currency_valuation/models/product_template.py create mode 100644 stock_currency_valuation/models/stock_move.py create mode 100644 stock_currency_valuation/models/stock_valuation_layer.py create mode 100644 stock_currency_valuation/views/product.xml create mode 100644 stock_currency_valuation/views/product_category.xml create mode 100644 stock_currency_valuation/views/stock_valuation_layer.xml create mode 100644 stock_currency_valuation/wizard/__init__,py create mode 100644 stock_currency_valuation/wizard/stock_valuation_layer_revaluation.py diff --git a/stock_currency_valuation/__init__.py b/stock_currency_valuation/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/stock_currency_valuation/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/stock_currency_valuation/__manifest__.py b/stock_currency_valuation/__manifest__.py new file mode 100644 index 000000000..5efadb631 --- /dev/null +++ b/stock_currency_valuation/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'Stock currency valuation', + 'version': "16.0.1.0.0", + 'category': 'Warehouse Management', + 'sequence': 14, + 'summary': '', + 'author': 'ADHOC SA', + 'website': 'www.adhoc.com.ar', + 'images': [ + ], + 'depends': [ + 'stock_account', + 'product_replenishment_cost', + ], + 'data': [ + 'views/product_category.xml', + 'views/product.xml', + 'views/stock_valuation_layer.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': False, + 'assets': { + }, + 'license': 'AGPL-3', +} diff --git a/stock_currency_valuation/models/__init__.py b/stock_currency_valuation/models/__init__.py new file mode 100644 index 000000000..1226f4470 --- /dev/null +++ b/stock_currency_valuation/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_category +from . import stock_valuation_layer +from . import product_product +from . import product_template +from . import stock_move diff --git a/stock_currency_valuation/models/product_category.py b/stock_currency_valuation/models/product_category.py new file mode 100644 index 000000000..d76a9d4f3 --- /dev/null +++ b/stock_currency_valuation/models/product_category.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class productCategory(models.Model): + + _inherit = 'product.category' + + valuation_currency_id = fields.Many2one( + 'res.currency', + string='Secondary Currency Valuation', + company_dependent=True, + ) diff --git a/stock_currency_valuation/models/product_product.py b/stock_currency_valuation/models/product_product.py new file mode 100644 index 000000000..80406efe0 --- /dev/null +++ b/stock_currency_valuation/models/product_product.py @@ -0,0 +1,18 @@ +from odoo import models, fields + + +class productProduct(models.Model): + + _inherit = 'product.product' + + valuation_currency_id = fields.Many2one( + related="categ_id.valuation_currency_id", + currency_field='valuation_currency_id' + ) + standard_price_in_currency = fields.Float( + 'Cost in currency', company_dependent=True, + groups="base.group_user", + ) + + def _change_standard_price(self, new_price): + super(productProduct, self.with_context(default_bypass_currency_valuation=True))._change_standard_price(new_price) diff --git a/stock_currency_valuation/models/product_template.py b/stock_currency_valuation/models/product_template.py new file mode 100644 index 000000000..af8661dd1 --- /dev/null +++ b/stock_currency_valuation/models/product_template.py @@ -0,0 +1,70 @@ +from odoo import models, fields, api + + +class productTemplate(models.Model): + + _inherit = 'product.template' + + valuation_currency_id = fields.Many2one( + related="categ_id.valuation_currency_id", + currency_field='valuation_currency_id' + ) + standard_price_in_currency = fields.Float( + 'Cost', compute='_compute_standard_price_in_currency', + inverse='_set_standard_price_in_currency', search='_search_standard_price_in_currency', + digits='Product Price', groups="base.group_user", + ) + replenishment_cost_type = fields.Selection( + selection_add=[('average_in_currency', 'Average Cost in Currency')], + ondelete={'average_in_currency': 'set default'} + ) + + @api.depends() + def _compute_replenishment_cost(self): + use_average_in_currency = self.filtered(lambda x: x.replenishment_cost_type == "average_in_currency") + super(productTemplate, self - use_average_in_currency)._compute_replenishment_cost() + if use_average_in_currency: + company_id = self.env.company + # domain = [ + # ('product_tmpl_id', 'in', use_average_in_currency.ids), + # ('company_id', '=', company_id.id), + # ] + # groups = self.env['stock.valuation.layer']._read_group(domain, ['value_in_currency:sum', 'quantity:sum'], + # ['product_tmpl_id']) + # products_avg_cost = {group['product_tmpl_id'][0]: group['value_in_currency'] / group['quantity'] + # if group['quantity'] else 0 for group in groups} + for rec in use_average_in_currency: + product_currency = rec.currency_id + #product_cost = products_avg_cost.get(rec.id) or 0 + price_unit = rec.valuation_currency_id._convert( + from_amount=rec.standard_price_in_currency, + to_currency=product_currency, + company=company_id, + date=fields.date.today(), + ) + rec.update({ + 'replenishment_base_cost_currency_id': rec.valuation_currency_id.id, + 'replenishment_base_cost_on_currency': price_unit, + 'replenishment_cost': price_unit, + 'replenishment_base_cost': rec.standard_price_in_currency, + }) + + @api.depends_context('company') + @api.depends('product_variant_ids', 'product_variant_ids.standard_price') + def _compute_standard_price_in_currency(self): + # Depends on force_company context because standard_price_in_currency is company_dependent + # on the product_product + unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1) + for template in unique_variants: + template.standard_price_in_currency = template.product_variant_ids.standard_price_in_currency + for template in (self - unique_variants): + template.standard_price_in_currency = 0.0 + + def _set_standard_price_in_currency(self): + for template in self: + if len(template.product_variant_ids) == 1: + template.product_variant_ids.standard_price_in_currency = template.standard_price_in_currency + + def _search_standard_price_in_currency(self, operator, value): + products = self.env['product.product'].search([('standard_price_in_currency', operator, value)], limit=None) + return [('id', 'in', products.mapped('product_tmpl_id').ids)] diff --git a/stock_currency_valuation/models/stock_move.py b/stock_currency_valuation/models/stock_move.py new file mode 100644 index 000000000..9e5068350 --- /dev/null +++ b/stock_currency_valuation/models/stock_move.py @@ -0,0 +1,87 @@ +from collections import defaultdict + +from odoo import models, fields +from odoo.tools.float_utils import float_compare, float_is_zero + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _account_entry_move(self, qty, description, svl_id, cost): + am_vals_list = super()._account_entry_move(qty, description, svl_id, cost) + layer = self.env['stock.valuation.layer'].browse(svl_id) + if layer.valuation_currency_id: + for am_vals in am_vals_list: + for line_id in am_vals['line_ids']: + sign = -1 if line_id[2]['balance'] < 0 else 1 + line_id[2].update({ + 'currency_id': layer.valuation_currency_id.id, + 'amount_currency': abs(layer.value_in_currency) * sign + }) + return am_vals_list + + def product_price_update_before_done(self, forced_qty=None): + super().product_price_update_before_done(forced_qty=forced_qty) + # Actualizo tambien el costo en moneda + tmpl_dict = defaultdict(lambda: 0.0) + # adapt standard price on incomming moves if the product cost_method is 'average' + std_price_update = {} + for move in self.filtered(lambda move: move._is_in() and move.with_company(move.company_id).product_id.categ_id.valuation_currency_id + and move.with_company(move.company_id).product_id.cost_method == 'average'): + product_tot_qty_available = move.product_id.sudo().with_company(move.company_id).quantity_svl + tmpl_dict[move.product_id.id] + rounding = move.product_id.uom_id.rounding + + valued_move_lines = move._get_in_move_lines() + qty_done = 0 + for valued_move_line in valued_move_lines: + qty_done += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id) + + qty = forced_qty or qty_done + if float_is_zero(product_tot_qty_available, precision_rounding=rounding): + new_std_price_in_currency = move._get_currency_price_unit() + elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ + float_is_zero(product_tot_qty_available + qty, precision_rounding=rounding): + new_std_price_in_currency = move._get_currency_price_unit() + else: + # Get the standard price + amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.with_company(move.company_id).standard_price_in_currency + new_std_price_in_currency = ((amount_unit * product_tot_qty_available) + (move._get_currency_price_unit() * qty)) / (product_tot_qty_available + qty) + + tmpl_dict[move.product_id.id] += qty_done + # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products + move.product_id.with_company(move.company_id.id).with_context(disable_auto_svl=True).sudo().write({'standard_price_in_currency': new_std_price_in_currency}) + + std_price_update[move.company_id.id, move.product_id.id] = new_std_price_in_currency + # adapt standard price on incomming moves if the product cost_method is 'fifo' + for move in self.filtered(lambda move: + move.with_company(move.company_id).product_id.cost_method == 'fifo' + and float_is_zero(move.product_id.sudo().quantity_svl, precision_rounding=move.product_id.uom_id.rounding)): + move.product_id.with_company(move.company_id.id).sudo().write({'standard_price_in_currency': move._get_currency_price_unit()}) + + def _get_currency_price_unit(self): + """ Returns the unit price from this stock move """ + self.ensure_one() + currency_id = self.company_id.currency_id + if hasattr(self, 'purchase_order_Line') and self.purchase_order_Line: + currency_id = self.purchase_order_Line.currency_id + if hasattr(self, 'sale_line_id') and self.sale_line_id: + currency_id = self.sale_line_id.currency_id + + price_unit = currency_id._convert( + from_amount=self.price_unit, + to_currency=self.product_id.categ_id.valuation_currency_id, + company=self.company_id, + date=fields.date.today(), + ) + precision = self.env['decimal.precision'].precision_get('Product Price') + # If the move is a return, use the original move's price unit. + if self.origin_returned_move_id and self.origin_returned_move_id.sudo().stock_valuation_layer_ids: + layers = self.origin_returned_move_id.sudo().stock_valuation_layer_ids + # dropshipping create additional positive svl to make sure there is no impact on the stock valuation + # We need to remove them from the computation of the price unit. + if self.origin_returned_move_id._is_dropshipped() or self.origin_returned_move_id._is_dropshipped_returned(): + layers = layers.filtered(lambda l: float_compare(l.value, 0, precision_rounding=l.product_id.uom_id.rounding) <= 0) + layers |= layers.stock_valuation_layer_ids + quantity = sum(layers.mapped("quantity")) + return sum(layers.mapped("value_in_currency")) / quantity if not float_is_zero(quantity, precision_rounding=layers.uom_id.rounding) else 0 + return price_unit if not float_is_zero(price_unit, precision) or self._should_force_price_unit() else self.product_id.standard_price_in_currency diff --git a/stock_currency_valuation/models/stock_valuation_layer.py b/stock_currency_valuation/models/stock_valuation_layer.py new file mode 100644 index 000000000..b575e0ad5 --- /dev/null +++ b/stock_currency_valuation/models/stock_valuation_layer.py @@ -0,0 +1,35 @@ +from odoo import models, fields, api, _ +from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError +from odoo.tools import float_compare + + +class StockValuationLayer(models.Model): + + _inherit = 'stock.valuation.layer' + + valuation_currency_id = fields.Many2one(related="categ_id.valuation_currency_id") + unit_cost_in_currency = fields.Monetary('Unit Value in currency', compute="_compute_other_currency_values", currency_field='valuation_currency_id', store=True) + value_in_currency = fields.Monetary('Total Value incurrency', compute="_compute_other_currency_values", currency_field='valuation_currency_id', store=True) + product_tmpl_id = fields.Many2one(store=True) + bypass_currency_valuation = fields.Boolean() + + @api.depends('categ_id', 'value', 'bypass_currency_valuation', 'stock_valuation_layer_id') + def _compute_other_currency_values(self): + for rec in self: + ##rec.stock_valuation_layer_id + + if not rec.bypass_currency_valuation and rec.valuation_currency_id: + rec.value_in_currency = rec.currency_id._convert( + from_amount=rec.value, + to_currency=rec.valuation_currency_id, + company=rec.company_id, + date=rec.create_date, + ) + if rec.quantity: + rec.unit_cost_in_currency = abs(rec.value_in_currency / rec.quantity) + else: + rec.unit_cost_in_currency = False + else: + rec.value_in_currency = False + rec.unit_cost_in_currency = False diff --git a/stock_currency_valuation/views/product.xml b/stock_currency_valuation/views/product.xml new file mode 100644 index 000000000..961300a89 --- /dev/null +++ b/stock_currency_valuation/views/product.xml @@ -0,0 +1,18 @@ + + + product.template.form + product.template + + + + + + + diff --git a/stock_currency_valuation/views/product_category.xml b/stock_currency_valuation/views/product_category.xml new file mode 100644 index 000000000..7f1cb1687 --- /dev/null +++ b/stock_currency_valuation/views/product_category.xml @@ -0,0 +1,13 @@ + + + product.category.form + product.category + + +
+ + + + +
+
diff --git a/stock_currency_valuation/views/stock_valuation_layer.xml b/stock_currency_valuation/views/stock_valuation_layer.xml new file mode 100644 index 000000000..d1da503ed --- /dev/null +++ b/stock_currency_valuation/views/stock_valuation_layer.xml @@ -0,0 +1,28 @@ + + + stock.valuation.layer.tree + stock.valuation.layer + + + + + + + + + + + stock.valuation.layer.form + stock.valuation.layer + + + + + + + + +
+
+
+
diff --git a/stock_currency_valuation/wizard/__init__,py b/stock_currency_valuation/wizard/__init__,py new file mode 100644 index 000000000..fc5fdb745 --- /dev/null +++ b/stock_currency_valuation/wizard/__init__,py @@ -0,0 +1 @@ +from . import stock_valuation_layer_revaluation diff --git a/stock_currency_valuation/wizard/stock_valuation_layer_revaluation.py b/stock_currency_valuation/wizard/stock_valuation_layer_revaluation.py new file mode 100644 index 000000000..57dc7e598 --- /dev/null +++ b/stock_currency_valuation/wizard/stock_valuation_layer_revaluation.py @@ -0,0 +1,22 @@ +from odoo import models + + +class StockValuationLayerRevaluation(models.TransientModel): + _inherit = 'stock.valuation.layer.revaluation' + + def action_validate_revaluation(self): + res = super().action_validate_revaluation() + # Update the stardard price in currency in case of AVCO + product_id = self.product_id.with_company(self.company_id) + if product_id.categ_id.property_cost_method in ('average', 'fifo') and product_id.categ_id.valuation_currency_id: + # Para actualizar el costo en currency vuelvo a calcular el valor en moneda + # Si bien hago dos veces el calculo (en el layer y aqui) esto es mas + # sencillo que obtener el ultimo layer y agregar el valor. + value_in_currency = self.currency_id._convert( + from_amount=self.added_value, + to_currency=product_id.categ_id.valuation_currency_id, + company=self.company_id, + date=self.create_date, + ) + product_id.with_context(disable_auto_svl=True).standard_price_in_currency += value_in_currency / self.current_quantity_svl + return res