diff --git a/stock_currency_valuation/__init__.py b/stock_currency_valuation/__init__.py index 9b4296142..daa2c3ffd 100644 --- a/stock_currency_valuation/__init__.py +++ b/stock_currency_valuation/__init__.py @@ -1,2 +1,2 @@ from . import models -from . import wizard +# from . import wizard diff --git a/stock_currency_valuation/__manifest__.py b/stock_currency_valuation/__manifest__.py index 1e3d96bc3..fc30536f0 100644 --- a/stock_currency_valuation/__manifest__.py +++ b/stock_currency_valuation/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Stock currency valuation", - "version": "18.0.1.1.0", + "version": "19.0.1.0.0", "category": "Warehouse Management", "sequence": 14, "summary": "", @@ -9,18 +9,20 @@ "images": [], "depends": [ "stock_account", - "stock_landed_costs", + # "stock_landed_costs", "product_replenishment_cost", ], "data": [ "views/product_category.xml", "views/stock_picking.xml", - "views/stock_landed_cost_views.xml", + # "views/stock_landed_cost_views.xml", "views/product.xml", - "views/stock_valuation_layer.xml", - "wizard/stock_valuation_layer_revaluation_views.xml", + "views/stock_move_views.xml", + #"views/product_value_views.xml", + # "views/stock_valuation_layer.xml", + # "wizard/stock_valuation_layer_revaluation_views.xml", ], - "installable": False, + "installable": True, "auto_install": False, "application": False, "assets": {}, diff --git a/stock_currency_valuation/models/__init__.py b/stock_currency_valuation/models/__init__.py index ebf7df320..da7c16f5a 100644 --- a/stock_currency_valuation/models/__init__.py +++ b/stock_currency_valuation/models/__init__.py @@ -1,7 +1,9 @@ from . import product_category -from . import stock_valuation_layer from . import product_product from . import product_template from . import stock_move -from . import stock_landed_cost from . import stock_picking +from . import product_value + +# from . import stock_valuation_layer +# from . import stock_landed_cost diff --git a/stock_currency_valuation/models/product_product.py b/stock_currency_valuation/models/product_product.py index c10df97b7..19a50a9ae 100644 --- a/stock_currency_valuation/models/product_product.py +++ b/stock_currency_valuation/models/product_product.py @@ -1,4 +1,9 @@ -from odoo import fields, models +from bisect import bisect +from collections import defaultdict + +from odoo import _, api, fields, models +from odoo.fields import Domain +from odoo.tools import SQL class productProduct(models.Model): @@ -13,3 +18,347 @@ class productProduct(models.Model): groups="base.group_user", help="Cost of the product expressed in the secondary currency defined on the product category. Used for inventory valuation and cost calculations in that currency.", ) + avg_cost_in_currency = fields.Monetary( + string="Average Cost (Currency)", + compute="_compute_value_in_currency", + compute_sudo=True, + currency_field="valuation_currency_id", + ) + total_value_in_currency = fields.Monetary( + string="Total Value (Currency)", + compute="_compute_value_in_currency", + compute_sudo=True, + currency_field="valuation_currency_id", + ) + + def write(self, vals): + old_price_in_currency = False + require_standard_price_compute = "standard_price" in vals and not self.env.context.get( + "disable_auto_revaluation" + ) + if "standard_price_in_currency" in vals and not self.env.context.get("disable_auto_revaluation"): + old_price_in_currency = {product: product.standard_price_in_currency for product in self} + res = super(productProduct, self.with_context(old_price_in_currency=old_price_in_currency)).write(vals) + if not require_standard_price_compute and old_price_in_currency: + self.with_context(old_price_in_currency=old_price_in_currency)._change_standard_price({}) + return res + + + @api.depends_context("to_date", "company", "warehouse_id") + @api.depends("cost_method", "stock_move_ids.value_in_currency", "standard_price_in_currency") + def _compute_value_in_currency(self): + # Only meaningful for products that have a secondary valuation currency. + # Products without one get zeroed out and we skip heavy computation for them. + for product in self.filtered(lambda p: not p.valuation_currency_id): + product.avg_cost_in_currency = 0 + product.total_value_in_currency = 0 + + products = self.filtered(lambda p: p.valuation_currency_id) + if not products: + return + + products = products._with_valuation_context() + + at_date = fields.Datetime.to_datetime(self.env.context.get("to_date")) + if at_date: + at_date = at_date.replace(hour=23, minute=59, second=59) + products = products.with_context(at_date=at_date) + + avg_cost_in_currency_by_product_id = {} + total_value_in_currency_by_product_id = {} + ratio_by_product_id = {} + + product_ids_grouped_by_cost_method = defaultdict(set) + for product in products: + if product.lot_valuated: + # lot-valuated: not handled here, zero out + avg_cost_in_currency_by_product_id[product.id] = 0 + total_value_in_currency_by_product_id[product.id] = 0 + continue + product_whole_company_context = product.with_context(warehouse_id=False) + if product.uom_id.is_zero(product.qty_available): + total_value_in_currency_by_product_id[product.id] = 0 + avg_cost_in_currency_by_product_id[product.id] = product.standard_price_in_currency + continue + if product.uom_id.is_zero(product_whole_company_context.qty_available): + total_value_in_currency_by_product_id[product.id] = ( + product.standard_price_in_currency * product.qty_available + ) + avg_cost_in_currency_by_product_id[product.id] = product.standard_price_in_currency + continue + if product.uom_id.compare(product.qty_available, product_whole_company_context.qty_available) != 0: + ratio_by_product_id[product.id] = ( + product.qty_available / product_whole_company_context.qty_available + ) + product_ids_grouped_by_cost_method[product.cost_method].add(product.id) + + for cost_method, product_ids in product_ids_grouped_by_cost_method.items(): + batch = self.env["product.product"].browse(product_ids).with_context(warehouse_id=False) + if cost_method == "standard": + avg_costs, total_values = batch._run_standard_batch_in_currency(at_date=at_date) + elif cost_method == "average": + avg_costs, total_values = batch._run_average_batch_in_currency( + at_date=at_date, force_recompute=True + ) + else: + avg_costs, total_values = batch._run_fifo_batch_in_currency(at_date=at_date) + avg_cost_in_currency_by_product_id.update(avg_costs) + total_value_in_currency_by_product_id.update(total_values) + + for product in self.filtered(lambda p: p.valuation_currency_id): + product.avg_cost_in_currency = avg_cost_in_currency_by_product_id.get( + product.id, product.standard_price_in_currency + ) + product.total_value_in_currency = total_value_in_currency_by_product_id.get( + product.id, 0 + ) * ratio_by_product_id.get(product.id, 1) + + # ------------------------------------------------------------------------- + # Private + # ------------------------------------------------------------------------- + + def _change_standard_price(self, old_price): + with_valuation_currency = self.filtered(lambda x: x.valuation_currency_id) + super(productProduct, self - with_valuation_currency)._change_standard_price(old_price) + old_price_in_currency = self.env.context.get("old_price_in_currency") or {} + for product in with_valuation_currency: + if product.cost_method == "fifo" or ( + product.standard_price == old_price.get(product) + and product.standard_price_in_currency == old_price_in_currency.get(product) + ): + continue + self.env["product.value"].sudo().create( + { + "product_id": product.id, + "value": product.standard_price, + "value_in_currency": product.standard_price_in_currency, + "valuation_currency_id": product.valuation_currency_id.id, + "company_id": product.company_id.id or self.env.company.id, + "date": fields.Datetime.now(), + "description": _( + "Price update from %(old_price)s to %(new_price)s by %(user)s", + old_price=old_price.get(product) or old_price_in_currency.get(product), + new_price=product.standard_price, + user=self.env.user.name, + ), + } + ) + return + + def _get_last_product_value(self, date=None, lot=False): + # estoy reemplazando el metodo para agregar value_in_currency. + # es mejor ir por un monkey patch? + domain = Domain( + [ + ("product_id", "in", self.ids), + ("move_id", "=", False), + ] + ) + if lot: + domain &= Domain(["|", ("lot_id", "=", lot.id), ("lot_id", "=", False)]) + else: + domain &= Domain([("lot_id", "=", False)]) + if date: + domain &= Domain([("date", "<=", date)]) + + query = self.env["product.value"].sudo()._search(domain) + query_select = SQL("distinct ON (product_value.product_id) product_value.id") + query.order = SQL("product_value.product_id, product_value.date DESC, product_value.id DESC") + query._ids = tuple(id_ for (id_,) in self.env.execute_query(query.select(query_select))) + product_values = self.env["product.value"].browse(query._ids) + product_values.sudo().fetch(["product_id", "value", "value_in_currency", "date"]) + return {pv.product_id: pv for pv in product_values} + + def _run_standard_batch_in_currency(self, at_date=None, lot=None): + """Equivalent of _run_standard_batch but using standard_price_in_currency. + At a given date reads the last recorded product.value.value_in_currency.""" + std_price_in_currency_by_product_id = {p.id: p.standard_price_in_currency for p in self} + if at_date: + product_value_by_product = self._get_last_product_value(at_date, lot=lot) + std_price_in_currency_by_product_id = { + p.id: ( + product_value_by_product[p].value_in_currency + if p in product_value_by_product + else p.standard_price_in_currency + ) + for p in self + } + value_by_product_id = { + p.id: p.qty_available * std_price_in_currency_by_product_id.get(p.id, 0) for p in self + } + return std_price_in_currency_by_product_id, value_by_product_id + + def _run_fifo_batch_in_currency(self, at_date=None, lot=None): + """Equivalent of _run_fifo_batch but for the secondary valuation currency. + Translates the FIFO stack value (company currency) to valuation_currency + using today's exchange rate.""" + std_price_in_currency_by_product_id = {} + value_in_currency_by_product_id = {} + for product in self: + quantity = product.qty_available + value = product._run_fifo(quantity, lot, at_date) + if value and product.valuation_currency_id: + value_in_currency = product.company_id.currency_id._convert( + from_amount=value, + to_currency=product.valuation_currency_id, + company=product.company_id, + date=fields.Date.today(), + ) + else: + value_in_currency = 0 + std_price = value_in_currency / quantity if quantity else 0 + std_price_in_currency_by_product_id[product.id] = std_price + value_in_currency_by_product_id[product.id] = value_in_currency + return std_price_in_currency_by_product_id, value_in_currency_by_product_id + + def _run_average_batch_in_currency(self, at_date=None, lot=None, force_recompute=False): + """Replica de _run_average_batch pero calculando el costo promedio en la moneda + secundaria (valuation_currency_id) usando value_in_currency de los moves. + + Retorna (std_price_in_currency_by_product_id, value_in_currency_by_product_id). + Solo procesa productos que tienen valuation_currency_id definido. + """ + std_price_in_currency_by_product_id = {} + value_in_currency_by_product_id = {} + quantity_by_product_id = {} + + # Solo tiene sentido para productos con moneda de valuación + products = self.filtered(lambda p: p.valuation_currency_id) + if not products: + return std_price_in_currency_by_product_id, value_in_currency_by_product_id + + if not at_date and not force_recompute: + std_price_in_currency_by_product_id = {p.id: p.standard_price_in_currency for p in products} + value_in_currency_by_product_id = { + p.id: p.qty_available * std_price_in_currency_by_product_id.get(p.id, 0) for p in products + } + return std_price_in_currency_by_product_id, value_in_currency_by_product_id + + moves_domain = Domain([ + ('product_id', 'in', products._as_query()), + ('company_id', '=', self.env.company.id), + '|', '|', ('is_in', '=', True), ('is_dropship', '=', True), ('is_out', '=', True), + ]) + if lot: + moves_domain &= Domain([('move_line_ids.lot_id', 'in', lot.id)]) + if at_date: + moves_domain &= Domain([('date', '<=', at_date)]) + + move_fields = [ + 'date', 'is_dropship', 'is_in', 'is_out', 'location_dest_id', 'location_id', + 'move_line_ids', 'picked', 'value', 'value_in_currency', 'product_id', + ] + last_manual_value_by_product = products._get_last_product_value(at_date, lot=lot) + oldest_manual_value = ( + min(pv.date for pv in last_manual_value_by_product.values()) + if last_manual_value_by_product else False + ) + if oldest_manual_value: + moves_domain &= Domain([('date', '>=', oldest_manual_value)]) + + moves = self.env['stock.move'].search_fetch( + moves_domain, + field_names=move_fields, + order='date, id', + ) + moves.move_line_ids.fetch([ + 'company_id', 'location_id', 'location_dest_id', 'lot_id', 'owner_id', 'picked', 'quantity_product_uom', + ]) + + moves_by_product = moves.grouped(key=lambda m: m.product_id) + + # Punto de partida: último valor manual registrado en product.value + for manual_value in last_manual_value_by_product.values(): + product = manual_value.product_id + quantity = product.with_context(to_date=manual_value.date).qty_available + + std_price_in_currency_by_product_id[product.id] = manual_value.value_in_currency + quantity_by_product_id[product.id] = quantity + value_in_currency_by_product_id[product.id] = manual_value.value_in_currency * quantity + + product_moves = moves_by_product.get(product, self.env['stock.move']) + index = bisect(product_moves, manual_value.date, key=lambda m: m.date) + moves_by_product[product] = product_moves[index:] + + # Reproducir el historial de valuación en moneda secundaria + for product, product_moves in moves_by_product.items(): + quantity = quantity_by_product_id.get(product.id, 0) + average_cost = std_price_in_currency_by_product_id.get(product.id, 0) + value = value_in_currency_by_product_id.get(product.id, 0) + + for move in product_moves: + if move.is_in or move.is_dropship: + in_qty = move._get_valued_qty() + in_value = move.value_in_currency + if lot: + lot_qty = move._get_valued_qty(lot) + in_value = (in_value * lot_qty / in_qty) if in_qty else 0 + in_qty = lot_qty + previous_qty = quantity + quantity += in_qty + if previous_qty > 0: + value += in_value + average_cost = value / quantity if quantity else average_cost + elif previous_qty <= 0: + average_cost = in_value / in_qty if in_qty else average_cost + value = average_cost * quantity + if move.is_out or move.is_dropship: + out_qty = move._get_valued_qty() + out_value = out_qty * average_cost + if lot: + lot_qty = move._get_valued_qty(lot) + out_value = (out_value * lot_qty / out_qty) if out_qty else 0 + out_qty = lot_qty + value -= out_value + quantity -= out_qty + + std_price_in_currency_by_product_id[product.id] = average_cost + value_in_currency_by_product_id[product.id] = value + + return std_price_in_currency_by_product_id, value_in_currency_by_product_id + + def _update_standard_price(self, extra_value=None, extra_quantity=None): + """Extiende _update_standard_price para también actualizar standard_price_in_currency + en productos que tienen valuation_currency_id definido.""" + super()._update_standard_price(extra_value=extra_value, extra_quantity=extra_quantity) + + products_with_currency = self.filtered(lambda p: p.valuation_currency_id and not p.lot_valuated) + if not products_with_currency: + return + + # Agrupar por cost_method para actualizar standard_price_in_currency + products_by_cost_method = defaultdict(lambda: self.env['product.product']) + for product in products_with_currency: + products_by_cost_method[product.cost_method] |= product + + for cost_method, products in products_by_cost_method.items(): + if cost_method == 'standard': + # Para precio estándar no hay recálculo automático de AVCO; + # el usuario debe actualizar standard_price_in_currency manualmente. + continue + + if cost_method == 'average': + new_prices_in_currency = products._run_average_batch_in_currency(force_recompute=True)[0] + for product in products: + if product.id in new_prices_in_currency: + product.with_context(disable_auto_revaluation=True).sudo().standard_price_in_currency = ( + new_prices_in_currency[product.id] + ) + continue + + if cost_method == 'fifo': + # Para FIFO: ratio entre standard_price_in_currency y standard_price + # proporcional al último precio de entrada, usando la tasa de la moneda actual. + for product in products: + qty_available = product._with_valuation_context().qty_available + if product.uom_id.compare(qty_available, 0) > 0 and product.standard_price: + # Calcular usando la tasa de cambio actual entre company_currency y valuation_currency + new_price_in_currency = product.company_id.currency_id._convert( + from_amount=product.standard_price, + to_currency=product.valuation_currency_id, + company=product.company_id, + date=fields.Date.today(), + ) + product.with_context(disable_auto_revaluation=True).sudo().standard_price_in_currency = ( + new_price_in_currency + ) diff --git a/stock_currency_valuation/models/product_value.py b/stock_currency_valuation/models/product_value.py new file mode 100644 index 000000000..8f97bf43f --- /dev/null +++ b/stock_currency_valuation/models/product_value.py @@ -0,0 +1,64 @@ +from odoo import api, fields, models + + +class ProductValue(models.Model): + _inherit = "product.value" + + valuation_currency_id = fields.Many2one("res.currency", compute="_compute_valuation_currency_id", store=True) + value_in_currency = fields.Monetary(string="Value", currency_field="valuation_currency_id") + + @api.depends("company_id", "move_id", "lot_id", "product_id") + def _compute_valuation_currency_id(self): + for product_value in self: + if product_value.move_id: + product_value.valuation_currency_id = product_value.move_id.product_id.with_company( + product_value.company_id + ).valuation_currency_id.id + elif product_value.lot_id: + product_value.valuation_currency_id = product_value.lot_id.product_id.with_company( + product_value.company_id + ).valuation_currency_id.id + elif product_value.product_id: + product_value.valuation_currency_id = product_value.product_id.with_company( + product_value.company_id + ).valuation_currency_id.id + + @api.model_create_multi + def create(self, vals_list): + """Override create to set default values from product's standard_price if not provided""" + for vals in vals_list: + # Obtener el producto según el contexto + product = None + if vals.get("product_id"): + product = self.env["product.product"].browse(vals["product_id"]) + elif vals.get("lot_id"): + lot = self.env["stock.lot"].browse(vals["lot_id"]) + product = lot.product_id + elif vals.get("move_id"): + move = self.env["stock.move"].browse(vals["move_id"]) + product = move.product_id + + if product: + # Obtener la compañía + company_id = vals.get("company_id") + if not company_id: + if vals.get("move_id"): + company_id = self.env["stock.move"].browse(vals["move_id"]).company_id.id + elif vals.get("lot_id"): + company_id = self.env["stock.lot"].browse(vals["lot_id"]).company_id.id + else: + company_id = self.env.company.id + + company = self.env["res.company"].browse(company_id) + product_with_company = product.with_company(company) + + # Si no está definido value, usar standard_price del producto + if "value" not in vals or not vals.get("value"): + vals["value"] = product_with_company.standard_price + + # Si no está definido value_in_currency y el producto tiene moneda de valuación, usar standard_price_in_currency + if ( + "value_in_currency" not in vals or not vals.get("value_in_currency") + ) and product_with_company.valuation_currency_id: + vals["value_in_currency"] = product_with_company.standard_price_in_currency + return super().create(vals_list) diff --git a/stock_currency_valuation/models/stock_move.py b/stock_currency_valuation/models/stock_move.py index 5fe09c929..db0a3e780 100644 --- a/stock_currency_valuation/models/stock_move.py +++ b/stock_currency_valuation/models/stock_move.py @@ -1,134 +1,180 @@ -from collections import defaultdict - from odoo import fields, models -from odoo.tools.float_utils import float_compare, float_is_zero class StockMove(models.Model): _inherit = "stock.move" - def _get_price_unit(self): - # Esto modifica el precio moneda de la compañia basandose en el valor la cotizacoin - # del dolar agregado en el picking. - # TODO: pude fallar si los lotes de un mismo producto tiene diferentes costos - self.ensure_one() - price_units = super()._get_price_unit() - for index in price_units.items(): - if ( - self.picking_id.currency_rate - and self.purchase_line_id.order_id.currency_id == self.picking_id.valuation_currency_id - ): - price_units[index[0]] = self.purchase_line_id.price_unit / self.picking_id.currency_rate - return price_units + valuation_currency_id = fields.Many2one( + related="product_id.valuation_currency_id", + ) + value_in_currency = fields.Monetary( + "Currency Value", + currency_field="valuation_currency_id", + help="The current value of the move. It's zero if the move is not valued.", + ) + value_manual_in_currency = fields.Monetary( + "Currency Manual Value", + currency_field="valuation_currency_id", + compute="_compute_value_manual", + inverse="_inverse_value_in_currency_manual", + ) + standard_price_in_currency = fields.Float( + related="product_id.standard_price_in_currency", string="Standard Price in currency" + ) + + def _inverse_value_in_currency_manual(self): + for move in self: + if move.value_manual_in_currency == move.value_in_currency: + continue + self.env["product.value"].create( + { + "move_id": move.id, + "value_in_currency": move.value_manual_in_currency, + "company_id": move.company_id.id, + } + ) - 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, - } + def _compute_value_manual(self): + super()._compute_value_manual() + for move in self: + move.value_manual_in_currency = move.value_in_currency + + def _set_value(self, correction_quantity=None): + super()._set_value(correction_quantity=correction_quantity) + for move in self: + if move.with_company(move.company_id).valuation_currency_id and move.value: + if move.picking_id and move.picking_id.currency_rate: + move.value_in_currency = move.value * move.picking_id.currency_rate + elif move.picking_id: + move.value_in_currency = move.with_company(move.company_id).company_id.currency_id._convert( + from_amount=move.value, + to_currency=move.valuation_currency_id, + company=move.company_id, + date=move.date, ) - 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 + # def _get_price_unit(self): + # # Esto modifica el precio moneda de la compañia basandose en el valor la cotizacoin + # # del dolar agregado en el picking. + # # TODO: pude fallar si los lotes de un mismo producto tiene diferentes costos + # self.ensure_one() + # price_units = super()._get_price_unit() + # for index in price_units.items(): + # if ( + # self.picking_id.currency_rate + # and self.purchase_line_id.order_id.currency_id == self.picking_id.valuation_currency_id + # ): + # price_units[index[0]] = self.purchase_line_id.price_unit / self.picking_id.currency_rate + # return price_units - 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 - ) + # 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 - 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( - default=move.product_id.standard_price_in_currency - ) - 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( - default=move.product_id.standard_price_in_currency - ) - 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) + # 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 - 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} - ) + # 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 + # ) - 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()} - ) + # 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( + # default=move.product_id.standard_price_in_currency + # ) + # 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( + # default=move.product_id.standard_price_in_currency + # ) + # 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) - def _get_currency_price_unit(self, default=0.0): - """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 + # 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} + # ) - 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 default + # 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, default=0.0): + # """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 default diff --git a/stock_currency_valuation/views/product_value_views.xml b/stock_currency_valuation/views/product_value_views.xml new file mode 100644 index 000000000..e6dd3fe92 --- /dev/null +++ b/stock_currency_valuation/views/product_value_views.xml @@ -0,0 +1,21 @@ + + + + + product.value.form.view.inherit.currency.valuation + product.value + + + + + + + + + + + diff --git a/stock_currency_valuation/views/stock_move_views.xml b/stock_currency_valuation/views/stock_move_views.xml new file mode 100644 index 000000000..c99a521ab --- /dev/null +++ b/stock_currency_valuation/views/stock_move_views.xml @@ -0,0 +1,35 @@ + + + + + stock.move.view.list.inherit.currency.valuation + stock.move + + + + + + + + + + + + stock.move.view.list.valuation.inherit.currency + stock.move + + + + + + + + +