oca-technical/odoo-bringout-oca-hr-expense-hr_expense_invoice/hr_expense_invoice/models/hr_expense.py
2025-08-29 15:43:03 +02:00

210 lines
8.3 KiB
Python

# Copyright 2017 Tecnativa - Vicent Cubells
# Copyright 2020 Tecnativa - David Vidal
# Copyright 2021 Tecnativa - Víctor Martínez
# Copyright 2015-2024 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import Command, _, api, fields, models
from odoo.exceptions import UserError
class HrExpense(models.Model):
_inherit = "hr.expense"
sheet_id_state = fields.Selection(related="sheet_id.state", string="Sheet state")
invoice_id = fields.Many2one(
comodel_name="account.move",
string="Vendor Bill",
domain=[
("move_type", "=", "in_invoice"),
("state", "=", "posted"),
("payment_state", "=", "not_paid"),
("expense_ids", "=", False),
],
copy=False,
)
transfer_move_ids = fields.One2many(
comodel_name="account.move",
inverse_name="source_invoice_expense_id",
)
def _prepare_invoice_values(self):
invoice_lines = [
(
0,
0,
{
"product_id": self.product_id.id,
"name": self.name,
"price_unit": self.untaxed_amount,
"quantity": self.quantity,
"account_id": self.account_id.id,
"analytic_distribution": self.analytic_distribution,
"tax_ids": [(6, 0, self.tax_ids.ids)],
},
)
]
return {
"name": "/",
"ref": self.reference,
"move_type": "in_invoice",
"invoice_date": self.date,
"invoice_line_ids": invoice_lines,
}
def action_move_create(self):
"""Don't let super to create any move:
- Paid by company: there's already the invoice.
- Paid by employee: we create here a journal entry transferring the AP
balance from the invoice partner to the employee.
"""
expenses_with_invoice = self.filtered("invoice_id")
res = super(HrExpense, self - expenses_with_invoice).action_move_create()
# Create AP transfer entry for expenses paid by employees
for expense in expenses_with_invoice:
if expense.payment_mode == "own_account":
move = self.env["account.move"].create(
expense._prepare_own_account_transfer_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: x.partner_id == self.invoice_id.partner_id
)
(ap_lines + transfer_line).reconcile()
return res
def _prepare_own_account_transfer_move_vals(self):
self.ensure_one()
self = self.with_company(self.company_id)
# TODO: Allow to select a specific journal
journal = self.env["account.journal"].search(
[
("company_id", "=", self.company_id.id),
("type", "=", "general"),
],
limit=1,
)
employee_partner = self.employee_id.sudo().address_home_id.commercial_partner_id
invoice_partner = self.invoice_id.partner_id
ap_lines = self.invoice_id.line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
amount_invoice = sum(ap_lines.mapped("credit"))
return {
"journal_id": journal.id,
"move_type": "entry",
"name": "/",
"date": self.date,
"ref": self.name,
"source_invoice_expense_id": self.id,
"line_ids": [
Command.create(
{
"account_id": ap_lines.account_id[:1].id,
"partner_id": invoice_partner.id,
"debit": amount_invoice,
}
),
Command.create(
{
"account_id": employee_partner.property_account_payable_id.id,
"partner_id": employee_partner.id,
"credit": amount_invoice,
}
),
],
}
def action_expense_create_invoice(self):
invoice = self.env["account.move"].create(self._prepare_invoice_values())
attachments = self.env["ir.attachment"].search(
[("res_model", "=", self._name), ("res_id", "in", self.ids)]
)
for attachment in attachments:
attachment.copy({"res_model": invoice._name, "res_id": invoice.id})
# Normalize data in the expense for avoiding mismatchings. Examples: the tax
# reported is not correct, there's more than one concept, etc...
self.write(
{
"invoice_id": invoice.id,
"quantity": 1,
"tax_ids": False,
"unit_amount": invoice.amount_total,
}
)
return True
@api.constrains("invoice_id")
def _check_invoice_id(self):
for expense in self: # Only non binding expense
if (
not expense.sheet_id
and expense.invoice_id
and expense.invoice_id.state != "posted"
):
raise UserError(_("Vendor bill state must be Posted"))
@api.onchange("invoice_id")
def _onchange_invoice_id(self):
"""Assure quantity is 1 if an invoice is set for having proper totals, and
the rest of the fields that are not computed writable, avoiding to ud
"""
if self.invoice_id:
self.quantity = 1
self.reference = self.invoice_id.name
self.date = self.invoice_id.date
if self.invoice_id.company_id != self.company_id:
# for avoiding to trigger dependent computes
self.company_id = self.invoice_id.company_id.id
# tax_ids put as dependency for assuring this is computed after setting tax_ids
@api.depends("invoice_id", "tax_ids")
def _compute_unit_amount(self):
with_invoice = self.filtered("invoice_id")
for record in with_invoice:
record.unit_amount = record.invoice_id.amount_total
return super(HrExpense, self - with_invoice)._compute_unit_amount()
# tax_ids put as dependency for assuring this is computed after setting tax_ids
@api.depends("invoice_id", "tax_ids")
def _compute_amount(self):
with_invoice = self.filtered("invoice_id")
for record in with_invoice:
record.total_amount = record.invoice_id.amount_total
return super(HrExpense, self - with_invoice)._compute_amount()
@api.depends("invoice_id")
def _compute_currency_id(self):
with_invoice = self.filtered("invoice_id")
for record in with_invoice:
record.currency_id = record.invoice_id.currency_id.id
return super(HrExpense, self - with_invoice)._compute_currency_id()
@api.depends("invoice_id")
def _compute_tax_ids(self):
with_invoice = self.filtered("invoice_id")
for record in with_invoice:
record.tax_ids = [(5,)]
return super(HrExpense, self - with_invoice)._compute_tax_ids()
@api.depends(
"transfer_move_ids.line_ids.amount_residual",
"transfer_move_ids.line_ids.amount_residual_currency",
)
def _compute_amount_residual(self):
"""Compute the amount residual for expenses paid by employee with invoices."""
applicable_expenses = self.filtered(lambda x: x.transfer_move_ids)
for rec in applicable_expenses:
if not rec.currency_id or rec.currency_id == rec.company_currency_id:
residual_field = "amount_residual"
else:
residual_field = "amount_residual_currency"
payment_term_lines = rec.transfer_move_ids.sudo().line_ids.filtered(
lambda x: x.account_type in ("asset_receivable", "liability_payable")
)
rec.amount_residual = -sum(payment_term_lines.mapped(residual_field))
return super(HrExpense, self - applicable_expenses)._compute_amount_residual()