Skip to content
Open
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
129 changes: 100 additions & 29 deletions hr_expense_invoice/models/hr_expense_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,109 @@ def get_expense_sheets_with_invoices(self, func):

def _do_create_ap_moves(self):
# Create AP transfer entry for expenses paid by employees
all_generated_moves = self.env["account.move"]
for expense in self.expense_line_ids.filtered("invoice_id"):
if expense.payment_mode == "own_account":
move_vals = expense._prepare_own_account_transfer_move_vals()
move = self.env["account.move"].create(move_vals)
move.action_post()
# reconcile with the invoice
ap_lines = expense.invoice_id.line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
transfer_line = move.line_ids.filtered(
lambda x, partner=expense.invoice_id.partner_id: x.partner_id
== partner
all_generated_moves |= move
return all_generated_moves

def _reconcile_ap_moves(self):
# Reconcile AP transfer entries with vendor bills once they are posted
for expense in self.expense_line_ids.filtered("invoice_id"):
if expense.payment_mode == "own_account":
move = self.account_move_ids.filtered(
lambda m, exp=expense: m.source_invoice_expense_id == exp
)
(ap_lines + transfer_line).reconcile()
if move and move.state == "posted":
ap_lines = expense.invoice_id.line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
transfer_line = move.line_ids.filtered(
lambda x, partner=expense.invoice_id.partner_id: x.partner_id
== partner
)
if ap_lines and transfer_line:
(ap_lines + transfer_line).reconcile()

def _do_create_moves(self):
removed_expenses_map = {}
sheets_to_skip_super = self.env["hr.expense.sheet"]
all_generated_moves = self.env["account.move"]

# 1. Ocultar temporalmente los gastos con factura
for sheet in self:
expenses_with_invoice = sheet.expense_line_ids.filtered("invoice_id")
if expenses_with_invoice:
if len(expenses_with_invoice) == len(sheet.expense_line_ids):
# Todos tienen factura, nos saltamos el super
sheets_to_skip_super += sheet
else:
# Reporte mixto: quitamos las líneas con factura temporalmente
removed_expenses_map[sheet.id] = expenses_with_invoice
sheet.expense_line_ids -= expenses_with_invoice

# 2. Llamar a super para que Odoo genere los asientos de las líneas normales
sheets_for_super = self - sheets_to_skip_super
if sheets_for_super:
res = super(HrExpenseSheet, sheets_for_super)._do_create_moves()
all_generated_moves |= res

# 3. Devolver las líneas con factura al reporte
for sheet_id, expenses in removed_expenses_map.items():
sheet = self.env["hr.expense.sheet"].browse(sheet_id)
sheet.expense_line_ids += expenses

# 4. Generar los movimientos AP (transferencias) para las líneas con factura
ap_moves = self._do_create_ap_moves()
all_generated_moves |= ap_moves

return all_generated_moves

def action_sheet_move_post(self):
# Handle expense sheets with invoices
sheets_all_inovices = self.get_expense_sheets_with_invoices(all)
res = super(HrExpenseSheet, self - sheets_all_inovices).action_sheet_move_post()
# Use 'any' here because there may be mixed sheets
# and we have to create ap moves for those invoices
for expense in self.get_expense_sheets_with_invoices(any):
expense._validate_expense_invoice()
expense._check_can_create_move()
expense._do_create_ap_moves()
# The payment state is set in a fixed way in super, but it depends on the
# payment state of the invoices when there are some of them linked
expense.filtered(
lambda x: x.expense_line_ids.invoice_id
and x.payment_mode == "company_account"
sheets_with_invoices = self.get_expense_sheets_with_invoices(any)
sheets_all_invoices = self.get_expense_sheets_with_invoices(all)

# 1. Validar que cuadren los importes ANTES de procesar nada
for sheet in sheets_with_invoices:
sheet._validate_expense_invoice()

# 2. El core de Odoo procesará las hojas sin facturas y las mixtas
# (El ocultamiento de líneas ocurrirá de forma segura
# dentro de _do_create_moves)
res = super(HrExpenseSheet, self - sheets_all_invoices).action_sheet_move_post()

# 3. Procesamiento manual para hojas 100% facturas (que saltan el super)
for sheet in sheets_all_invoices:
sheet._check_can_create_move()

moves = sheet._do_create_moves()
sheet.account_move_ids |= moves
moves.action_post()

sheet.filtered(
lambda x: x.payment_mode == "company_account"
)._compute_from_account_move_ids()

# Setear el estado igual que hace Odoo
if sheet.state == "approve" and sheet.account_move_ids:
if sheet.payment_state in ["paid", "in_payment"]:
sheet.state = "done"
else:
sheet.state = "post"

# 4. Refrescar estado de pago para hojas mixtas si fuera necesario
sheets_mixed = sheets_with_invoices - sheets_all_invoices
if sheets_mixed:
sheets_mixed.filtered(
lambda x: x.payment_mode == "company_account"
)._compute_from_account_move_ids()

# 5. Reconciliar los apuntes AP ahora que TODOS han sido publicados
for sheet in sheets_with_invoices:
sheet._reconcile_ap_moves()

return res

def set_to_paid(self):
Expand Down Expand Up @@ -108,11 +180,10 @@ def _prepare_bills_vals(self):
expenses_without_invoice = self.expense_line_ids.filtered(
lambda r: not r.invoice_id
)
if expenses_without_invoice:
res["line_ids"] = [
Command.create(expense._prepare_move_lines_vals())
for expense in expenses_without_invoice
]
res["line_ids"] = [
Command.create(expense._prepare_move_lines_vals())
for expense in expenses_without_invoice
]

return res

Expand Down Expand Up @@ -211,7 +282,7 @@ def _track_subtype(self, init_values):
super()._track_subtype(init_values)

def _check_can_create_move(self):
expense_sheets_with_invoices = self.get_expense_sheets_with_invoices(any)
expense_sheets_with_invoices = self.get_expense_sheets_with_invoices(all)
res = super(
HrExpenseSheet, self - expense_sheets_with_invoices
)._check_can_create_move()
Expand Down
31 changes: 31 additions & 0 deletions hr_expense_invoice/tests/test_hr_expense_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,34 @@ def test_6_hr_expense_mixed_invoice_same_sheet(self):
self.assertEqual(self.invoice2.payment_state, "paid")
# 2 ap moves and 1 vendor bill
self.assertEqual(len(sheet.account_move_ids), 3)

def test_7_coverage_boosters(self):
"""Test to cover edge cases for 100% Codecov coverage"""
sheet = self._action_submit_expenses(self.expense)

# 1. Cobertura para _track_subtype 'else' (Imagen 3)
sheet.write({"state": "draft"})
sheet._track_subtype({"state": "draft"})

# 2. Cobertura para los 'if' en _reconcile_ap_moves (Imagen 1)
self.invoice.action_post()
self.expense.write(
{
"invoice_id": self.invoice.id,
"payment_mode": "own_account",
}
)

# Condición False 1: No hay asientos generados (move = False)
sheet._reconcile_ap_moves()

# Condición False 2: El asiento existe, pero NO está publicado
dummy_move = self.env["account.move"].create(
{
"move_type": "entry",
"source_invoice_expense_id": self.expense.id,
"journal_id": self.cash_journal.id,
}
)
sheet.account_move_ids |= dummy_move
sheet._reconcile_ap_moves()
Loading