diff --git a/stock_currency_valuation/__manifest__.py b/stock_currency_valuation/__manifest__.py index 2d08a6924..51a42d8f1 100644 --- a/stock_currency_valuation/__manifest__.py +++ b/stock_currency_valuation/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Stock currency valuation', - 'version': "16.0.2.3.0", + 'version': "16.0.3.0.0", 'category': 'Warehouse Management', 'sequence': 14, 'summary': '', @@ -14,11 +14,13 @@ 'product_replenishment_cost', ], 'data': [ + 'security/ir.model.access.csv', 'views/product_category.xml', 'views/stock_picking.xml', 'views/stock_landed_cost_views.xml', 'views/product.xml', 'views/stock_valuation_layer.xml', + 'views/stock_valuation_layer_recompute.xml', 'wizard/stock_valuation_layer_revaluation_views.xml', ], 'installable': True, diff --git a/stock_currency_valuation/models/__init__.py b/stock_currency_valuation/models/__init__.py index f9cbb91a7..cfdd906b8 100644 --- a/stock_currency_valuation/models/__init__.py +++ b/stock_currency_valuation/models/__init__.py @@ -7,3 +7,4 @@ from . import account_move from . import stock_picking from . import stock_move_line +from . import stock_valuation_layer_recompute diff --git a/stock_currency_valuation/models/stock_valuation_layer_recompute.py b/stock_currency_valuation/models/stock_valuation_layer_recompute.py new file mode 100644 index 000000000..977bedbe3 --- /dev/null +++ b/stock_currency_valuation/models/stock_valuation_layer_recompute.py @@ -0,0 +1,416 @@ +from odoo.exceptions import UserError + +from odoo import Command, api, fields, models + + +def patch_check_reconciliation(self): + return +class StockValuationLayerRecompute(models.Model): + + _name = 'stock.valuation.layer.recompute' + _description = "layer recompute" + + + company_id = fields.Many2one('res.company', default=lambda self: self.env.company) + currency_id = fields.Many2one('res.currency', related='company_id.currency_id') + product_id = fields.Many2one( + 'product.product', + string='Product', + readonly=False + ) + valuation_currency_id = fields.Many2one( + 'res.currency', + string='Secondary Currency Valuation', + compute="_compute_valuation_currency_id" + ) + initial_amount = fields.Monetary() + final_amount = fields.Monetary() + initial_amount_in_currency = fields.Monetary() + final_amount_in_currency = fields.Monetary() + line_ids = fields.One2many( + comodel_name='stock.valuation.layer.recompute.line', + inverse_name='recompute_id', + ) + last_manual_svl_id = fields.Many2one('stock.valuation.layer') + amount_changed = fields.Boolean() + slv_changed = fields.Boolean() + final_rate = fields.Float( + string='Final Rate', + compute="_compute_final_rate", + store=True, + ) + + state = fields.Selection([ + ('draft', 'Draft'), + ('in_process', 'In Process'), + ('done', 'Done'), + ('no_change', 'Not changes Required'), + ('cancel', 'Cancelled') + ], default='draft', string='Status', required=True, readonly=True, copy=False) + + @api.depends('final_amount', 'final_amount_in_currency') + def _compute_final_rate(self): + for rec in self: + if rec.final_amount_in_currency: + rec.final_rate = rec.final_amount / rec.final_amount_in_currency + else: + rec.final_rate = 0.0 + + @api.onchange('product_id', 'company_id') + def _onchange_product_id(self): + self.line_ids = False + self.final_amount_in_currency = False + + @api.depends('product_id', 'company_id') + def _compute_valuation_currency_id(self): + for rec in self: + product_id = rec.product_id.with_company(rec.company_id) + rec.valuation_currency_id = product_id.categ_id.valuation_currency_id + + def action_cancel(self): + for rec in self: + rec.state = 'cancel' + + def back_to_draft(self): + for rec in self: + rec.state = 'draft' + + def delete_adjust_compute_lines(self): + manual_slv = self.env['stock.valuation.layer'].search( + [('company_id', '=', self.company_id.id), ('product_id', '=', self.product_id.id), ('create_uid', '=', 1), ('stock_move_id', '=', False)] + , order="create_date desc") + manual_slv.sudo().mapped('account_move_id').button_draft() + manual_slv.sudo().mapped('account_move_id').unlink() + manual_slv.sudo().unlink() + + self.action_compute_lines() + + def _prepare_new_values(self, from_currency_id, to_currency_id, qty, unit_cost, layer_date, manual_currency_rate=0): + is_company_currency = from_currency_id == self.company_id.currency_id + if manual_currency_rate and is_company_currency: + new_unit_cost_in_to_currency = unit_cost * manual_currency_rate + elif manual_currency_rate and not is_company_currency: + new_unit_cost_in_to_currency = unit_cost / manual_currency_rate + else: + new_unit_cost_in_to_currency = from_currency_id._convert( + from_amount=unit_cost, + to_currency=to_currency_id, + company=self.company_id, + date=layer_date, + ) + + if is_company_currency: + return [unit_cost, unit_cost * qty,new_unit_cost_in_to_currency, new_unit_cost_in_to_currency * qty,'manual rate' if manual_currency_rate else 'slv'] + return [new_unit_cost_in_to_currency,new_unit_cost_in_to_currency * qty,unit_cost,unit_cost * qty,'manual rate' if manual_currency_rate else 'slv'] + + def _get_standard_price(self, new_value, standard_price, quantity_at_time, quantity): + return (new_value + standard_price * (quantity_at_time - quantity)) / quantity_at_time if quantity_at_time else standard_price + + def action_compute_lines(self): + + last_manual_svl_id = self.env['stock.valuation.layer'].search( + [('company_id', '=', self.company_id.id), ('product_id', '=', self.product_id.id), ('create_uid', '=', 1), ('stock_move_id', '=', False)] + , order="create_date desc", limit=1) + self.last_manual_svl_id = last_manual_svl_id + + lines_to_zero = self.env.context.get('lines_to_zero', {}) + + leaf = [('company_id', '=', self.company_id.id), ('product_id', '=', self.product_id.id)] + svl_ids = self.env['stock.valuation.layer'].search(leaf, order="create_date asc") + lines = [Command.clear()] + quantity_at_time = 0 + standard_price_in_currency = 0 + standard_price = 0 + description = '' + + self.initial_amount_in_currency = self.product_id.with_company(self.company_id.id).standard_price_in_currency + self.initial_amount = self.product_id.with_company(self.company_id.id).standard_price + for svl_id in svl_ids: + svl_type = '' + vals = { + 'layer_id': svl_id.id, + 'layer_unit_cost': svl_id.unit_cost, + 'layer_value': svl_id.value, + 'layer_unit_cost_in_currency': svl_id.unit_cost_in_currency, + 'layer_value_in_currency': svl_id.value_in_currency, + } + quantity_at_time = quantity_at_time + svl_id.quantity + if svl_id.id in lines_to_zero: + new_unit_cost = 0 + new_value = 0 + new_unit_cost_in_currency = 0 + new_value_in_currency = 0 + quantity_at_time = quantity_at_time - svl_id.quantity + svl_type = 'zero' + + elif svl_id.stock_move_id and svl_id.stock_move_id.is_inventory: + standard_price_in_currency = standard_price_in_currency if standard_price_in_currency else svl_id.unit_cost_in_currency + standard_price = standard_price if standard_price else svl_id.unit_cost + + new_unit_cost = standard_price + new_value = standard_price * svl_id.quantity + new_unit_cost_in_currency = standard_price_in_currency + new_value_in_currency = standard_price_in_currency * svl_id.quantity + svl_type = 'inventory' + + # Si el movimento es de salida o de inventario, valor es el registrado en el producto + elif svl_id.stock_move_id and (svl_id.stock_move_id._is_out() ): + if len(lines) == 0: + standard_price_in_currency = svl_id.unit_cost_in_currency + standard_price = svl_id.unit_cost + else: + standard_price_in_currency = standard_price_in_currency + standard_price = standard_price + + new_unit_cost = standard_price + new_value = standard_price * svl_id.quantity + new_unit_cost_in_currency = standard_price_in_currency + new_value_in_currency = standard_price_in_currency * svl_id.quantity + svl_type = 'out' + + # es un ajuste? si no tiene movimiento de stock + elif not svl_id.stock_move_id: + new_value = svl_id.value + new_unit_cost = svl_id.unit_cost + # # before https://github.com/ingadhoc/stock/pull/675 + # if svl_id.create_date < fields.Datetime.from_string('2025-03-31 16:00:00'): + # new_unit_cost_in_currency = self.company_id.currency_id._convert( + # from_amount=new_unit_cost, + # to_currency=self.valuation_currency_id, + # company=self.company_id, + # date=svl_id.create_date, + # ) + # new_value_in_currency = self.company_id.currency_id._convert( + # from_amount=new_value, + # to_currency=self.valuation_currency_id, + # company=self.company_id, + # date=svl_id.create_date, + # ) + # else: + new_value_in_currency = svl_id.value_in_currency + new_unit_cost_in_currency = svl_id.unit_cost_in_currency + standard_price_in_currency = (new_value_in_currency + standard_price_in_currency * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price_in_currency + standard_price = (new_value + standard_price * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price + svl_type = 'ajustement' + + # Es una devolucion + elif svl_id.stock_move_id and svl_id.stock_move_id._is_returned(valued_type='in'): + # Si existe el movimiento de origen + # el valor de avco sale del mov de origen + + if svl_id.stock_move_id.origin_returned_move_id: + for temp_vals in lines: + if temp_vals[2] and temp_vals[2]['layer_id'] in svl_id.stock_move_id.origin_returned_move_id.stock_valuation_layer_ids.ids: + new_value_in_currency = temp_vals[2]['new_unit_cost_in_currency'] * svl_id.quantity + new_unit_cost_in_currency = temp_vals[2]['new_unit_cost_in_currency'] + standard_price_in_currency = (new_value_in_currency + standard_price_in_currency * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price_in_currency + new_value = temp_vals[2]['new_unit_cost'] * svl_id.quantity + new_unit_cost = temp_vals[2]['new_unit_cost'] + standard_price = (new_value + standard_price * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price + + svl_type = 'return of %s ' % temp_vals[2]['layer_id'] + + break + # sino sale de producto + else: + new_value_in_currency = standard_price_in_currency * svl_id.quantity + new_unit_cost_in_currency = standard_price_in_currency + new_value = standard_price * svl_id.quantity + new_unit_cost = standard_price + + svl_type = 'ret_without_move' + + + # landed cost + elif svl_id.stock_landed_cost_id: + new_value, new_unit_cost, new_value_in_currency, new_unit_cost_in_currency, description = self._prepare_new_values( + from_currency_id=self.company_id.currency_id, + to_currency_id=svl_id.stock_landed_cost_id.valuation_currency_id, + qty=svl_id.quantity, + unit_cost=svl_id.value, + layer_date=svl_id.create_date, + manual_currency_rate=svl_id.manual_currency_rate + ) + + standard_price = self._get_standard_price(new_value, standard_price, quantity_at_time, svl_id.quantity) + standard_price_in_currency = self._get_standard_price(new_value_in_currency, standard_price_in_currency, quantity_at_time, svl_id.quantity) + svl_type = 'landed cost %s' % description + # purchase refund + elif svl_id.stock_move_id and svl_id.stock_move_id.purchase_line_id and svl_id.stock_move_id.origin_returned_move_id: + for temp_vals in lines: + if temp_vals[2] and temp_vals[2]['layer_id'] in svl_id.stock_move_id.origin_returned_move_id.stock_valuation_layer_ids.ids: + new_value_in_currency = temp_vals[2]['new_unit_cost_in_currency'] * svl_id.quantity + new_unit_cost_in_currency = temp_vals[2]['new_unit_cost_in_currency'] + standard_price_in_currency = (new_value_in_currency + standard_price_in_currency * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price_in_currency + new_value = temp_vals[2]['new_unit_cost'] * svl_id.quantity + new_unit_cost = temp_vals[2]['new_unit_cost'] + standard_price = (new_value + standard_price * (quantity_at_time - svl_id.quantity)) / quantity_at_time if quantity_at_time else standard_price + + svl_type = 'purchase refund of %s ' % temp_vals[2]['layer_id'] + + break + + # purchase + elif svl_id.stock_move_id and svl_id.stock_move_id.purchase_line_id: + # purchase in company currency + if svl_id.stock_move_id.purchase_line_id.order_id.currency_id == self.company_id.currency_id: + new_unit_cost, new_value, new_unit_cost_in_currency, new_value_in_currency, description = self._prepare_new_values( + from_currency_id=self.company_id.currency_id, + to_currency_id= self.valuation_currency_id, + qty=svl_id.quantity, + unit_cost=svl_id.stock_move_id.purchase_line_id.price_unit, + layer_date=svl_id.create_date, + manual_currency_rate=svl_id.manual_currency_rate + ) + standard_price = self._get_standard_price(new_value, standard_price, quantity_at_time, svl_id.quantity) + standard_price_in_currency = self._get_standard_price(new_value_in_currency, standard_price_in_currency, quantity_at_time, svl_id.quantity) + svl_type = 'Purchase %s' % description + else: + new_unit_cost, new_value, new_unit_cost_in_currency, new_value_in_currency, description = self._prepare_new_values( + from_currency_id=self.valuation_currency_id, + to_currency_id=self.company_id.currency_id, + qty=svl_id.quantity, + unit_cost=svl_id.stock_move_id.purchase_line_id.price_unit, + layer_date=svl_id.create_date, + manual_currency_rate=svl_id.manual_currency_rate + ) + standard_price = self._get_standard_price(new_value, standard_price, quantity_at_time, svl_id.quantity) + standard_price_in_currency = self._get_standard_price(new_value_in_currency, standard_price_in_currency, quantity_at_time, svl_id.quantity) + svl_type = 'Purchase %s' % description + else: + new_unit_cost, new_value, new_unit_cost_in_currency, new_value_in_currency, description = self._prepare_new_values( + from_currency_id=self.company_id.currency_id, + to_currency_id=self.valuation_currency_id, + qty=svl_id.quantity, + unit_cost=svl_id.unit_cost, + layer_date=svl_id.create_date, + manual_currency_rate=svl_id.manual_currency_rate + ) + standard_price = self._get_standard_price(new_value, standard_price, quantity_at_time, svl_id.quantity) + standard_price_in_currency = self._get_standard_price(new_value_in_currency, standard_price_in_currency, quantity_at_time, svl_id.quantity) + svl_type = 'slv %s' % description + + # Si no queda producto el precio es 0 + # if quantity_at_time == 0: + # standard_price_in_currency = 0 + # standard_price = 0 + + vals['new_value'] = new_value + vals['new_unit_cost'] = new_unit_cost + vals['standard_price'] = standard_price + + vals['new_value_in_currency'] = new_value_in_currency + vals['new_unit_cost_in_currency'] = new_unit_cost_in_currency + vals['standard_price_in_currency'] = standard_price_in_currency + vals['quantity_at_time'] = quantity_at_time + vals['svl_type'] = svl_type + + need_change_1 = self.valuation_currency_id.compare_amounts(svl_id.value_in_currency, new_value_in_currency) != 0.0 + need_change_2 = self.valuation_currency_id.compare_amounts(svl_id.unit_cost_in_currency, new_unit_cost_in_currency) != 0.0 + need_change_3 = self.currency_id.compare_amounts(svl_id.value, new_value) != 0.0 + need_change_4 = self.currency_id.compare_amounts(svl_id.unit_cost, new_unit_cost) != 0.0 + + need_change_5 = svl_id.id > last_manual_svl_id.id or not last_manual_svl_id + need_change_6 = svl_id.id in lines_to_zero + vals['need_changes'] = True if (need_change_1 or need_change_2 or need_change_3 or need_change_4 or need_change_6) and need_change_5 else False + lines.append(Command.create(vals),) + self.line_ids = lines + self.final_amount_in_currency = standard_price_in_currency + self.final_amount = standard_price + self.state = 'in_process' + self.action_check_need_changes() + + def action_check_need_changes(self): + if not self.line_ids.filtered('need_changes') and \ + self.final_amount_in_currency == self.initial_amount_in_currency and \ + self.final_amount == self.initial_amount: + self.state = 'no_change' + + def action_manual_slv_revaluation(self): + #orig_check_reconciliation = AccountMoveLine._check_reconciliation + #AccountMoveLine._check_reconciliation = patch_check_reconciliation + slv_changed = False + # to_reconciled_line_ids guarda todas las conciliaciones + to_reconciled_line_ids = [] + for line_id in self.line_ids.filtered('need_changes'): + slv_changed = True + if line_id.layer_id.stock_landed_cost_id: + raise UserError('No puedo ajustar un landed cost') + line_id.layer_id.write({ + 'unit_cost': line_id.new_unit_cost, + 'value':line_id.new_value + }) + line_id.layer_id.write({ + 'unit_cost_in_currency': line_id.new_unit_cost_in_currency, + 'value_in_currency':line_id.new_value_in_currency, + }) + lines = [] + product_move_line_ids = line_id.layer_id.account_move_id.line_ids.filtered(lambda x: x.product_id == self.product_id) + #agrego las full reconiliaciones + to_reconciled_line_ids.append(product_move_line_ids.full_reconcile_id.reconciled_line_ids) + move_ids = product_move_line_ids.mapped('move_id') + move_ids.button_draft() + for move_line in product_move_line_ids: + multiplier = 1 if move_line.credit == 0 else -1 + field = 'debit' if move_line.credit == 0 else 'credit' + # OJO el valor de currency puede ser positivo o negativo + # pero enel asiento siempre es positivo para debit y negativo para credit + # por eso multiplico el valor absoluto por el signo + lines.append(Command.update(move_line.id,{ + 'amount_currency': abs(line_id.new_value_in_currency) * multiplier, + field: abs(line_id.new_value), + })) + if lines: + line_id.layer_id.account_move_id.line_ids = lines + move_ids.action_post() + + if self.product_id.with_company(self.company_id.id).standard_price_in_currency != self.final_amount_in_currency: + self.product_id.with_company(self.company_id.id).with_context( + disable_auto_svl=True + ).sudo().write({'standard_price_in_currency': self.final_amount_in_currency}) + self.amount_changed = True + if self.product_id.with_company(self.company_id.id).standard_price != self.final_amount: + self.product_id.with_company(self.company_id.id).with_context( + disable_auto_svl=True + ).sudo().write({'standard_price': self.final_amount}) + self.amount_changed = True + + self.slv_changed = slv_changed + for to_reconciled_lines in to_reconciled_line_ids: + # si no tiene reconciliaciones, lo reconcilio sino supongo + # que ya lo reconcilie en antes + if not any(to_reconciled_lines.mapped('reconciled')): + to_reconciled_lines.reconcile() + #AccountMoveLine._check_reconciliation = orig_check_reconciliation + self.state = 'done' + + +class StockValuationLayerRecomputeLine(models.Model): + + _name = 'stock.valuation.layer.recompute.line' + _description = "lines layer recompute" + + + layer_id = fields.Many2one('stock.valuation.layer') + quantity = fields.Float(related="layer_id.quantity") + quantity_at_time = fields.Float() + layer_date = fields.Datetime(related="layer_id.create_date") + recompute_id = fields.Many2one('stock.valuation.layer.recompute') + currency_id = fields.Many2one('res.currency', related='recompute_id.valuation_currency_id') + company_currency_id = fields.Many2one('res.currency', related='recompute_id.currency_id') + + # company currency + layer_unit_cost = fields.Monetary(currency_field="company_currency_id") + layer_value = fields.Monetary(currency_field="company_currency_id") + new_unit_cost = fields.Monetary(currency_field="company_currency_id") + new_value = fields.Monetary(currency_field="company_currency_id") + standard_price = fields.Monetary(currency_field="company_currency_id") + + # Secundary currency + layer_unit_cost_in_currency = fields.Monetary() + layer_value_in_currency = fields.Monetary() + new_unit_cost_in_currency = fields.Monetary() + new_value_in_currency = fields.Monetary() + standard_price_in_currency = fields.Monetary() + need_changes = fields.Boolean() + svl_type = fields.Char() diff --git a/stock_currency_valuation/security/ir.model.access.csv b/stock_currency_valuation/security/ir.model.access.csv new file mode 100644 index 000000000..687592cae --- /dev/null +++ b/stock_currency_valuation/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +stock_currency_valuation.access_stock_valuation_layer_recompute,access_stock_valuation_layer_recompute,stock_currency_valuation.model_stock_valuation_layer_recompute,base.group_user,1,1,1,1 +stock_currency_valuation.access_stock_valuation_layer_recompute_line,access_stock_valuation_layer_recompute_line,stock_currency_valuation.model_stock_valuation_layer_recompute_line,base.group_user,1,1,1,1 diff --git a/stock_currency_valuation/views/stock_valuation_layer_recompute.xml b/stock_currency_valuation/views/stock_valuation_layer_recompute.xml new file mode 100644 index 000000000..3673b6134 --- /dev/null +++ b/stock_currency_valuation/views/stock_valuation_layer_recompute.xml @@ -0,0 +1,114 @@ + + + + stock.valuation.layer.recompute.form + stock.valuation.layer.recompute + form + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + stock.valuation.layer.recompute.list + stock.valuation.layer.recompute + tree + + + + + + + + + + + + + + + + + + stock.valuation.layer.recompute.search + stock.valuation.layer.recompute + search + + + + + + + + + + + + + + + + + + + + Recompute layer + ir.actions.act_window + stock.valuation.layer.recompute + tree,form + + + + +
diff --git a/stock_currency_valuation/wizard/stock_valuation_layer_revaluation_views.xml b/stock_currency_valuation/wizard/stock_valuation_layer_revaluation_views.xml index a0776b840..9061fcc74 100644 --- a/stock_currency_valuation/wizard/stock_valuation_layer_revaluation_views.xml +++ b/stock_currency_valuation/wizard/stock_valuation_layer_revaluation_views.xml @@ -11,7 +11,7 @@
= ( by ) Use a negative added value to record a decrease in the product value -
+ diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index ad53d277d..3f384b589 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -2,7 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError from odoo.tools import float_compare @@ -184,7 +184,7 @@ def check_cancel(self): if self.filtered( lambda x: x.picking_id and x.state == 'cancel' and not self.user_has_groups('stock_ux.allow_picking_cancellation')): raise ValidationError("Only User with 'Picking cancelation allow' rights can cancel pickings") - + def _merge_moves(self, merge_into=False): # 22/04/2024: Agregamos esto porque sino al intentar confirmar compras con usuarios sin permisos, podia pasar que salga la constrain de arriba (check_cancel) return super(StockMove,self.with_context(cancel_from_order=True))._merge_moves(merge_into = merge_into)