Skip to content
Closed

[WIP] #851

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ class ExchangeDifferenceWizard(models.TransientModel):
domain=[("type", "=", "sale")],
check_company=True,
)
fiscal_position = fields.Selection(
[("automatic", "Automatic"), ("manual", "Manual")],
default="automatic",
string="Fiscal Position Mode",
required=True,
help="If automatic, fiscal position will be auto detected for each customer, if manual you can force one or none fiscal position.",
)
fiscal_position_id = fields.Many2one(
"account.fiscal.position",
string="Fiscal Position",
help="Fiscal position to use for all the customers.",
)

@api.model
def default_get(self, fields):
Expand All @@ -36,7 +48,10 @@ def default_get(self, fields):
# Recuperamos las líneas de movimiento
# por ahora directamente filtramos los que ya se procesaron
move_lines = self.env["account.move.line"].search(
[("move_id.exchange_reversal_id", "=", False), ("move_id.exchange_reversed_move_ids", "=", False)]
[
("move_id.exchange_reversal_id", "=", False),
("move_id.exchange_reversed_move_ids", "=", False),
]
+ [("id", "in", move_line_ids)]
)

Expand Down Expand Up @@ -110,6 +125,7 @@ def _create_invoice_and_reconcile(self, journal):
)

exch_moves = partner_to_moves.get(rec.partner_id.id, self.env["account.move"])

exch_moves.write({"exchange_reversal_id": move.id})

move.action_post()
Expand All @@ -120,7 +136,7 @@ def _create_invoice_and_reconcile(self, journal):
debit_credit_note = (
self.env["account.move"]
.with_context(exchange_diff_account_receivable_id=rec_account.id)
.create(rec._prepare_debit_credit_note(journal=journal))
.create(rec._prepare_debit_credit_note(exch_moves, journal))
)
if debit_credit_note.currency_id.round(debit_credit_note.amount_total) < 0:
# switch to credit note if the amount is negative
Expand Down Expand Up @@ -221,15 +237,141 @@ def _get_receivable_account(self):

return rec_account

def _prepare_debit_credit_note(self, journal):
def _prepare_invoice_lines_by_taxes(self, invoice_lines, account, amount):
"""
Agrupa las líneas de factura por combinación de impuestos y prorratea el balance.
Retorna una lista de tuplas (0, 0, vals) para crear las líneas de la nota de débito/crédito.
"""
self.ensure_one()

product = self.env.company.exchange_difference_product
partner = self.partner_id

# Si no hay líneas de factura, retornamos una única línea con el balance total
if not invoice_lines:
return [
(
0,
0,
{
"account_id": account.id,
"quantity": 1.0,
"price_unit": self.balance,
"partner_id": partner.id,
"product_id": product.id,
},
)
]

# En contexto de localización argentina, filtramos los impuestos que no son IVA
# (aquellos cuyo tax_group_id no tiene l10n_ar_vat_afip_code seteado)
is_argentina = self.env["account.tax.group"]._fields.get("l10n_ar_vat_afip_code")

tax_groups = {}
for line in invoice_lines:
if is_argentina:
# Solo consideramos impuestos de IVA (con l10n_ar_vat_afip_code seteado)
taxes = line.tax_ids.filtered("tax_group_id.l10n_ar_vat_afip_code")
other_taxes = line.tax_ids - taxes
else:
taxes = line.tax_ids
tax_key = frozenset(taxes.ids)
if tax_key not in tax_groups:
tax_groups[tax_key] = {
"tax_ids": taxes,
"subtotal": 0.0,
}
if not self.wizard_id.fiscal_position_id:
tax_groups[tax_key]["subtotal"] += line.price_total
else:
tax_groups[tax_key]["subtotal"] += line.price_total - sum(other_taxes.mapped("amount"))

# Calcular el total de subtotales para el prorrateo
total_subtotal = sum(group["subtotal"] for group in tax_groups.values())
# total_subtotal = sum(invoice_lines.mapped("price_total"))
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este código comentado debe eliminarse antes del merge. Si se consideró este cálculo alternativo, la decisión de usar el enfoque actual debería estar documentada en un comentario conciso o en el historial de commits.

Suggested change
# total_subtotal = sum(invoice_lines.mapped("price_total"))

Copilot uses AI. Check for mistakes.

# Si el total es cero, distribuimos equitativamente
if total_subtotal == 0:
return [
(
0,
0,
{
"account_id": account.id,
"quantity": 1.0,
"price_unit": amount,
"partner_id": partner.id,
"product_id": product.id,
},
)
]

# Crear las líneas prorrateadas
invoice_line_vals = []
remaining_balance = amount
groups_list = list(tax_groups.values())

for i, group in enumerate(groups_list):
if i == len(groups_list) - 1:
# Última línea: usar el saldo restante para evitar diferencias de redondeo
price_unit = remaining_balance
else:
# Prorratear según el peso del subtotal
proportion = group["subtotal"] / total_subtotal
price_unit = self.wizard_id.company_id.currency_id.round(amount * proportion)
remaining_balance -= price_unit

invoice_line_vals.append(
(
0,
0,
{
"account_id": account.id,
"quantity": 1.0,
"price_unit": price_unit,
"partner_id": partner.id,
"product_id": product.id,
"tax_ids": [(6, 0, group["tax_ids"].ids)],
},
)
)

return invoice_line_vals

def _prepare_debit_credit_note(self, exch_moves, journal):
"""
Retorna un diccionario con los datos para crear la nota de débito/crédito
Prorratea el balance entre las diferentes combinaciones de impuestos presentes en las líneas de factura
"""
self.ensure_one()

company = self.wizard_id.company_id
partner = self.partner_id
account = self.env["account.move.line"]._get_exchange_account(company, self.balance)

# approach si hacemos una sola linea
# partial_reconciles = self.env["account.partial.reconcile"].search(
# [("exchange_move_id", "in", exch_moves.ids)]
# )
# invoice_lines = (partial_reconciles.mapped("debit_move_id") + partial_reconciles.mapped("credit_move_id")).mapped("move_id").filtered(lambda move: move.is_sale_document()).mapped("invoice_line_ids")
# invoice_line_vals = self._prepare_invoice_lines_by_taxes(invoice_lines, account, self.balance)
Comment on lines +352 to +357
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El código comentado debe eliminarse antes del merge. Si estos enfoques alternativos son necesarios para referencia futura, deberían documentarse en el historial de commits o en un comentario más conciso que explique la decisión de diseño, no dejando código completo comentado.

Copilot uses AI. Check for mistakes.

# en este approach iteramos sobre cada exhcange diff y generamos lineas para cada grupo de impuestos
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error ortográfico en el comentario: "exhcange" debería ser "exchange".

Suggested change
# en este approach iteramos sobre cada exhcange diff y generamos lineas para cada grupo de impuestos
# en este approach iteramos sobre cada exchange diff y generamos lineas para cada grupo de impuestos

Copilot uses AI. Check for mistakes.
invoice_line_vals = []
for exchange_move in exch_moves:
partial_reconcile = self.env["account.partial.reconcile"].search(
[("exchange_move_id", "=", exchange_move.ids)]
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El operador de comparación está incorrecto. Se utiliza "=" (igualdad) cuando debería usarse "in" (pertenencia) ya que exchange_move.ids es una lista. Esto causará un error en tiempo de ejecución porque el dominio esperaría comparar un campo con un valor escalar, no con una lista.

Suggested change
[("exchange_move_id", "=", exchange_move.ids)]
[("exchange_move_id", "in", exchange_move.ids)]

Copilot uses AI. Check for mistakes.
)
Comment on lines +361 to +364
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Se está ejecutando una búsqueda (search) dentro de un bucle for, lo cual es ineficiente. Esto genera N consultas a la base de datos (una por cada exchange_move). Sería más eficiente realizar una única búsqueda con el dominio [("exchange_move_id", "in", exch_moves.ids)] antes del bucle y luego iterar sobre los resultados agrupados, o usar un grouped/mapped si es posible.

Suggested change
for exchange_move in exch_moves:
partial_reconcile = self.env["account.partial.reconcile"].search(
[("exchange_move_id", "=", exchange_move.ids)]
)
# Buscar todos los partial_reconcile de una sola vez
all_partial_reconciles = self.env["account.partial.reconcile"].search(
[("exchange_move_id", "in", exch_moves.ids)]
)
# Agrupar por exchange_move_id
partials_by_exchange = {}
for pr in all_partial_reconciles:
exchange_id = pr.exchange_move_id.id
partials_by_exchange.setdefault(exchange_id, self.env["account.partial.reconcile"].browse()). |= pr
for exchange_move in exch_moves:
partial_reconcile = partials_by_exchange.get(exchange_move.id, self.env["account.partial.reconcile"].browse())

Copilot uses AI. Check for mistakes.
invoice_lines = (
(partial_reconcile.debit_move_id + partial_reconcile.credit_move_id)
.mapped("move_id")
.filtered(lambda move: move.is_sale_document())
.mapped("invoice_line_ids")
)
invoice_line_vals += self._prepare_invoice_lines_by_taxes(
invoice_lines, account, exchange_move.amount_total_signed
)

invoice_vals = {
"move_type": "out_invoice",
"currency_id": company.currency_id.id,
Expand All @@ -239,21 +381,15 @@ def _prepare_debit_credit_note(self, journal):
"journal_id": journal.id,
"invoice_origin": "Ajuste por diferencia de cambio",
"invoice_payment_term_id": False,
"invoice_line_ids": [
(
0,
0,
{
"account_id": account.id,
"quantity": 1.0,
"price_unit": self.balance,
"partner_id": partner.id,
"product_id": self.env.company.exchange_difference_product.id,
},
)
],
"invoice_line_ids": invoice_line_vals,
}

if self.wizard_id.fiscal_position == "manual":
if self.wizard_id.fiscal_position_id:
invoice_vals["fiscal_position_id"] = self.wizard_id.fiscal_position_id.id
else:
invoice_vals["fiscal_position_id"] = False

# hack para evitar modulo glue con l10n_latam_document
# hasta el momento tenemos feedback de dos clientes uruguayos de que los ajustes por intereses
# se hacen comoo factura normal y no ND. Si eventualmente otros clintes solicitan ND tendremos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<form>
<group>
<field name="journal_id"/>
<field name="fiscal_position"/>
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El campo fiscal_position debería incluir el atributo string para definir la etiqueta del campo en la interfaz de usuario. Esto mejora la claridad y mantiene la consistencia con otros campos del formulario.

Copilot uses AI. Check for mistakes.
<field name="fiscal_position_id" invisible="fiscal_position != 'manual'"/>
<field name="line_ids">
<list editable="bottom" create="false" edit="false">
<field name="partner_id"/>
Expand Down