diff --git a/l10n_ve_accountant/__manifest__.py b/l10n_ve_accountant/__manifest__.py index 0a38cbf0..bb60469a 100644 --- a/l10n_ve_accountant/__manifest__.py +++ b/l10n_ve_accountant/__manifest__.py @@ -7,7 +7,7 @@ "author": "binaural-dev", "website": "https://binauraldev.com/", "category": "Accounting/Localizations/Account Chart", - "version": "17.0.0.0.28", + "version": "17.0.0.0.30", "depends": [ "base", "web", diff --git a/l10n_ve_accountant/models/__init__.py b/l10n_ve_accountant/models/__init__.py index bb25fada..40f24c29 100644 --- a/l10n_ve_accountant/models/__init__.py +++ b/l10n_ve_accountant/models/__init__.py @@ -9,5 +9,6 @@ from . import account_payment_term from . import tax_unit from . import res_currency +from . import fields # from . import account_journal # ESTA HERENCIA NO SE IMPORTARÁ PORQUE ESTÁ GENERANDO ERROR, AL SOLUCIONAR, VOLVER A AGREGAR EN EN IMPORT diff --git a/l10n_ve_accountant/models/account_move.py b/l10n_ve_accountant/models/account_move.py index 26f56fa3..ca68a627 100644 --- a/l10n_ve_accountant/models/account_move.py +++ b/l10n_ve_accountant/models/account_move.py @@ -111,7 +111,6 @@ def _onchange_move_type(self): foreign_rate = fields.Float( compute="_compute_rate", - digits="Tasa", store=True, tracking=True, readonly=False, @@ -119,7 +118,6 @@ def _onchange_move_type(self): foreign_inverse_rate = fields.Float( help="Rate that will be used as factor to multiply of the foreign currency for this move.", compute="_compute_rate", - digits=(16, 15), store=True, index=True, readonly=False, @@ -177,6 +175,7 @@ def _compute_inverse_rate_vef(self): date = move.invoice_date or move.date if date: + rate = Rate.search([ ('currency_id', '=', currency.id), ('name', '<=', date), diff --git a/l10n_ve_accountant/models/account_move_line.py b/l10n_ve_accountant/models/account_move_line.py index f20bc8f1..f63b8d78 100644 --- a/l10n_ve_accountant/models/account_move_line.py +++ b/l10n_ve_accountant/models/account_move_line.py @@ -2,16 +2,17 @@ from odoo import api, fields, models, Command, _ from odoo.tools import float_compare from odoo.exceptions import UserError, ValidationError -from odoo.tools import frozendict, formatLang, format_date, float_compare, Query +from odoo.tools import frozendict, formatLang, format_date, float_compare, Query, float_round + from datetime import date, timedelta import traceback from markupsafe import Markup +from odoo.tools import float_is_zero import logging _logger = logging.getLogger(__name__) - class AccountMoveLine(models.Model): _inherit = "account.move.line" @@ -78,6 +79,19 @@ class AccountMoveLine(models.Model): help="When setted, this field will be used to fill the foreign credit field", ) + foreign_amount_residual = fields.Monetary( + currency_field="foreign_currency_id", + compute="_compute_foreign_amount_residuals", + store=True, + readonly=True, + ) + foreign_amount_residual_currency = fields.Monetary( + currency_field="foreign_currency_id", + compute="_compute_foreign_amount_residuals", + store=True, + readonly=True, + ) + @api.onchange("amount_currency", "currency_id") def _inverse_amount_currency(self): for line in self: @@ -288,10 +302,10 @@ def _calculate_for_non_invoice(self, line): line.company_id.currency_id, foreign_currency, line.company_id, - line.date + line.move_id.origin_payment_advanced_payment_id.date if line.move_id.origin_payment_advanced_payment_id else line.date #asientos de cruce toman tasa del pago ) - - inverse_rate_to_use = rate + + inverse_rate_to_use = rate if inverse_rate_to_use <= 0.0 else rate balance = sum(foreign_lines.mapped("amount_currency")) if balance and len(currency_lines) == 1: @@ -613,3 +627,303 @@ def write(self, vals): }) return res + + def _get_manual_foreign_amount_residual( + self, + currency=None, + foreign_debit=None, + foreign_credit=None, + partials=None, + ): + """ + Calculates the manual residual amount in foreign currency for the move line. + Considers matched partials and computes the remaining amount after reconciliation. + Returns a tuple: (rounded residual, manual_available). + """ + self.ensure_one() + + currency = currency or self.foreign_currency_id + if not currency: + return 0.0, False + + if self.foreign_currency_id and currency != self.foreign_currency_id: + return 0.0, False + + foreign_debit = foreign_debit if foreign_debit is not None else self.foreign_debit + foreign_credit = ( + foreign_credit if foreign_credit is not None else self.foreign_credit + ) + + manual_total = (foreign_debit or 0.0) - (foreign_credit or 0.0) + + partials = partials or (self.matched_debit_ids | self.matched_credit_ids) + manual_partial_total = 0.0 + for partial in partials: + amount = 0.0 + partial_currency = False + if partial.debit_move_id == self: + amount = partial.debit_amount_currency or 0.0 + partial_currency = partial.debit_currency_id + elif partial.credit_move_id == self: + amount = partial.credit_amount_currency or 0.0 + partial_currency = partial.credit_currency_id + + if partial_currency == currency and amount: + manual_partial_total += amount + + if currency.is_zero(manual_total) and currency.is_zero(manual_partial_total): + return 0.0, False + + if currency.is_zero(manual_total): + balance_sign = 1 if self.balance >= 0 else -1 + else: + balance_sign = 1 if manual_total >= 0 else -1 + + residual = manual_total - balance_sign * manual_partial_total + if balance_sign > 0: + residual = max(residual, 0.0) + else: + residual = min(residual, 0.0) + + return currency.round(residual), True + + @api.depends( + "foreign_currency_id", + "foreign_debit", + "foreign_credit", + "matched_debit_ids.debit_amount_currency", + "matched_debit_ids.debit_currency_id", + "matched_credit_ids.credit_amount_currency", + "matched_credit_ids.credit_currency_id", + ) + def _compute_foreign_amount_residuals(self): + """ + Computes and stores the foreign currency residuals for each move line. + Uses _get_manual_foreign_amount_residual for calculation. + """ + for line in self: + residual, manual_available = line._get_manual_foreign_amount_residual() + line.foreign_amount_residual = residual if manual_available else 0.0 + line.foreign_amount_residual_currency = residual if manual_available else 0.0 + + def _prepare_reconciliation_single_partial( + self, + debit_vals, + credit_vals, + shadowed_aml_values=None, + **kwargs, + ): + """ + Prepares the values for a single partial reconciliation between debit and credit lines. + Calculates foreign currency progress and updates partial values accordingly. + Returns the result from the super method with updated foreign currency fields. + """ + debit_line = debit_vals.get("aml") + credit_line = credit_vals.get("aml") + + initial_debit_amount_residual = debit_vals.get("amount_residual") + if initial_debit_amount_residual is None and debit_line: + initial_debit_amount_residual = getattr( + debit_line, "amount_residual", 0.0 + ) + initial_credit_amount_residual = credit_vals.get("amount_residual") + if initial_credit_amount_residual is None and credit_line: + initial_credit_amount_residual = getattr( + credit_line, "amount_residual", 0.0 + ) + + initial_debit_foreign_residual = debit_vals.get("foreign_amount_residual") + if initial_debit_foreign_residual is None and debit_line: + initial_debit_foreign_residual = getattr( + debit_line, "foreign_amount_residual", 0.0 + ) + initial_debit_foreign_residual_currency = debit_vals.get( + "foreign_amount_residual_currency" + ) + if initial_debit_foreign_residual_currency is None and debit_line: + initial_debit_foreign_residual_currency = getattr( + debit_line, "foreign_amount_residual_currency", 0.0 + ) + + initial_credit_foreign_residual = credit_vals.get("foreign_amount_residual") + if initial_credit_foreign_residual is None and credit_line: + initial_credit_foreign_residual = getattr( + credit_line, "foreign_amount_residual", 0.0 + ) + initial_credit_foreign_residual_currency = credit_vals.get( + "foreign_amount_residual_currency" + ) + if initial_credit_foreign_residual_currency is None and credit_line: + initial_credit_foreign_residual_currency = getattr( + credit_line, "foreign_amount_residual_currency", 0.0 + ) + + res = super()._prepare_reconciliation_single_partial( + debit_vals, + credit_vals, + shadowed_aml_values=shadowed_aml_values, + **kwargs, + ) + + partial_vals = res.get("partial_values") + if not partial_vals: + return res + + new_debit_amount_residual = debit_vals.get("amount_residual") + new_credit_amount_residual = credit_vals.get("amount_residual") + + def _compute_foreign_progress( + line, + initial_company_residual, + new_company_residual, + initial_foreign_residual, + initial_foreign_residual_currency, + ): + if not line or not line.foreign_currency_id: + return 0.0, abs(initial_foreign_residual or 0.0), 0.0, abs( + initial_foreign_residual_currency or 0.0 + ) + + company_foreign_currency = line.company_id.currency_foreign_id + foreign_currency = line.foreign_currency_id + + initial_company = abs(initial_company_residual or 0.0) + new_company = abs(new_company_residual or 0.0) + line_foreign_total = 0.0 + # For credit line, use foreign_credit directly for currency calculation + if line and line.foreign_credit is not None: + line_foreign_total = abs(line.foreign_credit) + elif line and line.foreign_debit is not None: + line_foreign_total = abs(line.foreign_debit) + initial_foreign = line_foreign_total or abs(initial_foreign_residual or 0.0) + if initial_foreign_residual_currency is not None: + initial_foreign_currency = abs(initial_foreign_residual_currency) + else: + initial_foreign_currency = initial_foreign + + if line_foreign_total: + initial_foreign_currency = line_foreign_total + + if not initial_company: + return ( + 0.0, + initial_foreign, + 0.0, + initial_foreign_currency, + ) + + matched_ratio = min( + max((initial_company - new_company) / initial_company, 0.0), + 1.0, + ) + + partial_foreign_currency = foreign_currency.round( + initial_foreign_currency * matched_ratio + ) + new_foreign_currency = foreign_currency.round( + max(initial_foreign_currency - partial_foreign_currency, 0.0) + ) + + rounding_currency = company_foreign_currency or foreign_currency + partial_foreign = rounding_currency.round(initial_foreign * matched_ratio) + new_foreign = rounding_currency.round( + max(initial_foreign - partial_foreign, 0.0) + ) + + return ( + partial_foreign, + new_foreign, + partial_foreign_currency, + new_foreign_currency, + ) + + ( + debit_foreign_amount, + debit_foreign_residual, + debit_foreign_amount_currency, + debit_foreign_residual_currency, + ) = _compute_foreign_progress( + debit_line, + initial_debit_amount_residual, + new_debit_amount_residual, + initial_debit_foreign_residual, + initial_debit_foreign_residual_currency, + ) + + ( + credit_foreign_amount, + credit_foreign_residual, + credit_foreign_amount_currency, + credit_foreign_residual_currency, + ) = _compute_foreign_progress( + credit_line, + initial_credit_amount_residual, + new_credit_amount_residual, + initial_credit_foreign_residual, + initial_credit_foreign_residual_currency, + ) + + company_foreign_currency = False + line_for_company = debit_line or credit_line + if line_for_company: + company_foreign_currency = line_for_company.company_id.currency_foreign_id + + preferred_candidates = [] + if company_foreign_currency: + if ( + debit_line + and debit_line.foreign_currency_id == company_foreign_currency + and debit_foreign_amount_currency + ): + preferred_candidates.append(debit_foreign_amount_currency) + if ( + credit_line + and credit_line.foreign_currency_id == company_foreign_currency + and credit_foreign_amount_currency + ): + preferred_candidates.append(credit_foreign_amount_currency) + + partial_foreign_amount = 0.0 + if preferred_candidates: + partial_foreign_amount = max(preferred_candidates) + else: + fallback_candidates = [] + rounding = False + if company_foreign_currency: + rounding = company_foreign_currency.rounding + elif line_for_company: + rounding = line_for_company.company_currency_id.rounding + + for amount in (debit_foreign_amount, credit_foreign_amount): + if amount and not ( + rounding + and float_is_zero( + amount, + precision_rounding=rounding, + ) + ): + fallback_candidates.append(amount) + + if fallback_candidates: + partial_foreign_amount = max(fallback_candidates) + + partial_vals.update( + { + "foreign_amount": partial_foreign_amount, + "debit_foreign_amount_currency": debit_foreign_amount_currency, + "credit_foreign_amount_currency": credit_foreign_amount_currency, + } + ) + + if debit_line and debit_line.foreign_currency_id: + debit_vals["foreign_amount_residual"] = debit_foreign_residual + debit_vals["foreign_amount_residual_currency"] = ( + debit_foreign_residual_currency + ) + + if credit_line and credit_line.foreign_currency_id: + credit_vals["foreign_amount_residual"] = credit_foreign_residual + credit_vals["foreign_amount_residual_currency"] = ( + credit_foreign_residual_currency + ) + return res \ No newline at end of file diff --git a/l10n_ve_accountant/models/account_partial_reconcile.py b/l10n_ve_accountant/models/account_partial_reconcile.py index c3139be6..a81f15c1 100644 --- a/l10n_ve_accountant/models/account_partial_reconcile.py +++ b/l10n_ve_accountant/models/account_partial_reconcile.py @@ -14,3 +14,33 @@ class AccountPartialReconcile(models.Model): store=True, index=True, ) + + foreign_currency_id = fields.Many2one( + "res.currency", + related="company_id.currency_foreign_id", + store=True, + ) + debit_foreign_currency_id = fields.Many2one( + "res.currency", + related="debit_move_id.foreign_currency_id", + store=True, + precompute=True, + ) + credit_foreign_currency_id = fields.Many2one( + "res.currency", + related="credit_move_id.foreign_currency_id", + store=True, + precompute=True, + ) + foreign_amount = fields.Monetary( + currency_field="foreign_currency_id", + help="Always positive amount concerned by this matching expressed in the company's foreign currency.", + ) + debit_foreign_amount_currency = fields.Monetary( + currency_field="debit_foreign_currency_id", + help="Always positive amount concerned by this matching expressed in the debit line foreign currency.", + ) + credit_foreign_amount_currency = fields.Monetary( + currency_field="credit_foreign_currency_id", + help="Always positive amount concerned by this matching expressed in the credit line foreign currency.", + ) \ No newline at end of file diff --git a/l10n_ve_accountant/models/account_payment.py b/l10n_ve_accountant/models/account_payment.py index 8d8aba02..67541aa5 100644 --- a/l10n_ve_accountant/models/account_payment.py +++ b/l10n_ve_accountant/models/account_payment.py @@ -26,14 +26,14 @@ def default_alternate_currency(self): foreign_rate = fields.Float( compute="_compute_rate", - digits="Tasa", + default=0.0, store=True, readonly=False, ) foreign_inverse_rate = fields.Float( help="Rate that will be used as factor to multiply of the foreign currency for this move.", compute="_compute_rate", - digits=(16, 15), + default=0.0, store=True, readonly=False, ) @@ -72,6 +72,7 @@ def _synchronize_to_moves(self, changed_fields): } ) return res + @api.depends("date") def _compute_rate(self): diff --git a/l10n_ve_accountant/models/fields.py b/l10n_ve_accountant/models/fields.py new file mode 100644 index 00000000..57da2a66 --- /dev/null +++ b/l10n_ve_accountant/models/fields.py @@ -0,0 +1,47 @@ +from odoo.fields import Monetary +from odoo.tools import float_repr +from odoo import tools +import logging + +_logger = logging.getLogger(__name__) + +def new_convert_to_column(self, value, record, values=None, validate=True): + + currency_field_name = self.get_currency_field(record) + currency_field = record._fields[currency_field_name] + + if values and currency_field_name in values: + dummy = record.new({currency_field_name: values[currency_field_name]}) + currency = dummy[currency_field_name] + elif values and currency_field.related and currency_field.related.split('.')[0] in values: + related_field_name = currency_field.related.split('.')[0] + dummy = record.new({related_field_name: values[related_field_name]}) + currency = dummy[currency_field_name] + else: + currency = record[:1].with_context(prefetch_fields=False)[currency_field_name] + currency = currency.with_env(record.env) + + value = float(value or 0.0) + + if currency: + + return float_repr(value, 10) + + return value + + +def new_convert_to_cache(self, value, record, validate=True): + value = float(value or 0.0) + if value and validate: + currency_field = self.get_currency_field(record) + currency = record.sudo().with_context(prefetch_fields=False)[currency_field] + if len(currency) > 1: + raise ValueError("Got multiple currencies while assigning values of monetary field %s" % str(self)) + elif currency: + value = value + return value + + +Monetary.convert_to_cache = new_convert_to_cache + +Monetary.convert_to_column = new_convert_to_column diff --git a/l10n_ve_accountant/models/res_currency.py b/l10n_ve_accountant/models/res_currency.py index ffd1e9cb..3fa306d0 100644 --- a/l10n_ve_accountant/models/res_currency.py +++ b/l10n_ve_accountant/models/res_currency.py @@ -1,9 +1,11 @@ from odoo import models, fields, api, _ +from odoo.exceptions import UserError import logging _logger = logging.getLogger(__name__) - +from odoo.fields import Monetary +from odoo.tools import float_repr class ResCurrency(models.Model): _inherit = "res.currency" @@ -21,3 +23,30 @@ def _compute_edit_rate(self): "l10n_ve_accountant.group_fiscal_config_support" ) ) + + def _convert(self, from_amount, to_currency, company=None, date=None, round=True): + + self, to_currency = self or to_currency, to_currency or self + assert self, "convert amount from unknown currency" + assert to_currency, "convert amount to unknown currency" + if from_amount: + to_amount = from_amount * self._get_conversion_rate(self, to_currency, company, date) + else: + return 0.0 + + return to_amount + + + def round(self, amount): + + self.ensure_one() + + try: + amount_float = float(amount) + except (ValueError, TypeError): + return super(ResCurrency, self).round(amount) + + if abs(amount_float - round(amount_float, 6)) > 1e-9: + return amount_float + + return super(ResCurrency, self).round(amount) \ No newline at end of file