Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
7
payroll_account/models/__init__.py
Normal file
7
payroll_account/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_payslip_line
|
||||
from . import hr_payslip_run
|
||||
from . import hr_payslip
|
||||
from . import hr_salary_rule
|
||||
14
payroll_account/models/hr_contract.py
Normal file
14
payroll_account/models/hr_contract.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
# В Odoo 19 «контракт» — это версия сотрудника (hr.version).
|
||||
_inherit = "hr.version"
|
||||
|
||||
analytic_account_id = fields.Many2one(
|
||||
"account.analytic.account", "Analytic Account"
|
||||
)
|
||||
journal_id = fields.Many2one("account.journal", "Salary Journal")
|
||||
279
payroll_account/models/hr_payslip.py
Normal file
279
payroll_account/models/hr_payslip.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = "hr.payslip"
|
||||
|
||||
# В Odoo 19 контракт — это hr.version; журнал берём из версии, но
|
||||
# оставляем поле редактируемым (compute+store+readonly=False) с fallback
|
||||
# на журнал типа 'general', чтобы проведение не падало из-за пустого
|
||||
# журнала (в отличие от жёсткого related в исходном 18.0-модуле).
|
||||
journal_id = fields.Many2one(
|
||||
"account.journal", string="Salary Journal",
|
||||
compute="_compute_journal_id", store=True, readonly=False)
|
||||
date = fields.Date(
|
||||
"Date Account",
|
||||
help="Keep empty to use the period of the validation(Payslip) date.",
|
||||
)
|
||||
move_id = fields.Many2one(
|
||||
"account.move", "Accounting Entry", readonly=True, copy=False
|
||||
)
|
||||
|
||||
@api.depends("contract_id")
|
||||
def _compute_journal_id(self):
|
||||
for slip in self:
|
||||
journal = slip.contract_id.journal_id
|
||||
if not journal:
|
||||
journal = self.env["account.journal"].search(
|
||||
[("type", "=", "general"),
|
||||
("company_id", "=", slip.company_id.id)], limit=1)
|
||||
slip.journal_id = journal
|
||||
|
||||
@api.onchange("contract_id")
|
||||
def onchange_contract(self):
|
||||
res = super().onchange_contract()
|
||||
self.journal_id = (
|
||||
self.contract_id.journal_id.id
|
||||
or (
|
||||
not self.contract_id
|
||||
and self.default_get(["journal_id"]).get("journal_id")
|
||||
)
|
||||
or self.journal_id
|
||||
)
|
||||
return res
|
||||
|
||||
def _remove_account_move(self):
|
||||
"""Удалить или сторнировать связанную проводку account.move."""
|
||||
for payslip in self:
|
||||
if not payslip.move_id:
|
||||
continue
|
||||
if not payslip.move_id.journal_id.restrict_mode_hash_table:
|
||||
payslip.move_id.with_context(force_delete=True).button_cancel()
|
||||
payslip.move_id.with_context(force_delete=True).unlink()
|
||||
else:
|
||||
payslip.move_id._reverse_moves()
|
||||
payslip.move_id = False
|
||||
|
||||
def action_payslip_cancel(self):
|
||||
self._remove_account_move()
|
||||
return super().action_payslip_cancel()
|
||||
|
||||
def action_payslip_draft(self):
|
||||
# Возврат в черновик снимает проводку (в Odoo 19 draft — штатный путь).
|
||||
self._remove_account_move()
|
||||
return super().action_payslip_draft()
|
||||
|
||||
def action_payslip_done(self):
|
||||
res = super().action_payslip_done()
|
||||
|
||||
for slip in self:
|
||||
line_ids = []
|
||||
debit_sum = 0.0
|
||||
credit_sum = 0.0
|
||||
date = slip.date or slip.date_to
|
||||
currency = (
|
||||
slip.company_id.currency_id or slip.journal_id.company_id.currency_id
|
||||
)
|
||||
|
||||
name = _("Payslip of %s") % (slip.employee_id.name)
|
||||
move_dict = {
|
||||
"narration": name,
|
||||
"ref": slip.number,
|
||||
"journal_id": slip.journal_id.id,
|
||||
"date": date,
|
||||
}
|
||||
for line in slip.line_ids:
|
||||
amount = currency.round(slip.credit_note and -line.total or line.total)
|
||||
if currency.is_zero(amount):
|
||||
continue
|
||||
debit_account_id = line.salary_rule_id.account_debit.id
|
||||
credit_account_id = line.salary_rule_id.account_credit.id
|
||||
|
||||
move_line_analytic_ids = {}
|
||||
if slip.contract_id.analytic_account_id:
|
||||
move_line_analytic_ids.update(
|
||||
{line.slip_id.contract_id.analytic_account_id.id: 100}
|
||||
)
|
||||
elif line.salary_rule_id.analytic_account_id:
|
||||
move_line_analytic_ids.update(
|
||||
{line.salary_rule_id.analytic_account_id.id: 100}
|
||||
)
|
||||
|
||||
if debit_account_id:
|
||||
debit_line = self._prepare_debit_line(
|
||||
line, amount, date, debit_account_id, move_line_analytic_ids
|
||||
)
|
||||
line_ids.append((0, 0, debit_line))
|
||||
debit_sum += debit_line["debit"] - debit_line["credit"]
|
||||
|
||||
if credit_account_id:
|
||||
credit_line = self._prepare_credit_line(
|
||||
line,
|
||||
amount,
|
||||
date,
|
||||
credit_account_id,
|
||||
move_line_analytic_ids,
|
||||
)
|
||||
line_ids.append((0, 0, credit_line))
|
||||
credit_sum += credit_line["credit"] - credit_line["debit"]
|
||||
|
||||
if currency.compare_amounts(credit_sum, debit_sum) == -1:
|
||||
acc_id = slip.journal_id.default_account_id.id
|
||||
if not acc_id:
|
||||
raise UserError(
|
||||
_(
|
||||
'The Expense Journal "%s" has not properly '
|
||||
"configured the Credit Account!"
|
||||
)
|
||||
% (slip.journal_id.name)
|
||||
)
|
||||
adjust_credit = self._prepare_adjust_credit_line(
|
||||
currency, credit_sum, debit_sum, slip.journal_id, date
|
||||
)
|
||||
line_ids.append([0, 0, adjust_credit])
|
||||
|
||||
elif currency.compare_amounts(debit_sum, credit_sum) == -1:
|
||||
acc_id = slip.journal_id.default_account_id.id
|
||||
if not acc_id:
|
||||
raise UserError(
|
||||
_(
|
||||
'The Expense Journal "%s" has not properly '
|
||||
"configured the Debit Account!"
|
||||
)
|
||||
% (slip.journal_id.name)
|
||||
)
|
||||
adjust_debit = self._prepare_adjust_debit_line(
|
||||
currency, credit_sum, debit_sum, slip.journal_id, date
|
||||
)
|
||||
line_ids.append([0, 0, adjust_debit])
|
||||
|
||||
if len(line_ids) > 0:
|
||||
move_dict["line_ids"] = line_ids
|
||||
move = self.env["account.move"].create(move_dict)
|
||||
slip.write({"move_id": move.id, "date": date})
|
||||
move.action_post()
|
||||
else:
|
||||
logger.info(
|
||||
f"Payslip {slip.number} did not generate any account move lines"
|
||||
)
|
||||
return res
|
||||
|
||||
def _prepare_debit_line(
|
||||
self, line, amount, date, debit_account_id, move_line_analytic_ids
|
||||
):
|
||||
tax_ids, tax_tag_ids, tax_repartition_line_id = self._get_tax_details(line)
|
||||
return {
|
||||
"name": line.name,
|
||||
"partner_id": line._get_partner_id(credit_account=False),
|
||||
"account_id": debit_account_id,
|
||||
"journal_id": line.slip_id.journal_id.id,
|
||||
"date": date,
|
||||
"debit": amount > 0.0 and amount or 0.0,
|
||||
"credit": amount < 0.0 and -amount or 0.0,
|
||||
"analytic_distribution": move_line_analytic_ids,
|
||||
"tax_line_id": line.salary_rule_id.account_tax_id.id,
|
||||
"tax_ids": tax_ids,
|
||||
"tax_repartition_line_id": tax_repartition_line_id,
|
||||
"tax_tag_ids": tax_tag_ids,
|
||||
}
|
||||
|
||||
def _prepare_credit_line(
|
||||
self, line, amount, date, credit_account_id, move_line_analytic_ids
|
||||
):
|
||||
tax_ids, tax_tag_ids, tax_repartition_line_id = self._get_tax_details(line)
|
||||
return {
|
||||
"name": line.name,
|
||||
"partner_id": line._get_partner_id(credit_account=True),
|
||||
"account_id": credit_account_id,
|
||||
"journal_id": line.slip_id.journal_id.id,
|
||||
"date": date,
|
||||
"debit": amount < 0.0 and -amount or 0.0,
|
||||
"credit": amount > 0.0 and amount or 0.0,
|
||||
"analytic_distribution": move_line_analytic_ids,
|
||||
"tax_line_id": line.salary_rule_id.account_tax_id.id,
|
||||
"tax_ids": tax_ids,
|
||||
"tax_repartition_line_id": tax_repartition_line_id,
|
||||
"tax_tag_ids": tax_tag_ids,
|
||||
}
|
||||
|
||||
def _prepare_adjust_credit_line(
|
||||
self, currency, credit_sum, debit_sum, journal, date
|
||||
):
|
||||
acc_id = journal.default_account_id.id
|
||||
return {
|
||||
"name": _("Adjustment Entry"),
|
||||
"partner_id": False,
|
||||
"account_id": acc_id,
|
||||
"journal_id": journal.id,
|
||||
"date": date,
|
||||
"debit": 0.0,
|
||||
"credit": currency.round(debit_sum - credit_sum),
|
||||
}
|
||||
|
||||
def _prepare_adjust_debit_line(
|
||||
self, currency, credit_sum, debit_sum, journal, date
|
||||
):
|
||||
acc_id = journal.default_account_id.id
|
||||
return {
|
||||
"name": _("Adjustment Entry"),
|
||||
"partner_id": False,
|
||||
"account_id": acc_id,
|
||||
"journal_id": journal.id,
|
||||
"date": date,
|
||||
"debit": currency.round(credit_sum - debit_sum),
|
||||
"credit": 0.0,
|
||||
}
|
||||
|
||||
def _get_tax_details(self, line):
|
||||
tax_ids = False
|
||||
tax_tag_ids = []
|
||||
salary_rule = line.salary_rule_id
|
||||
if salary_rule.tax_line_ids:
|
||||
account_tax_ids = [
|
||||
salary_rule_id.account_tax_id.id
|
||||
for salary_rule_id in salary_rule.tax_line_ids
|
||||
]
|
||||
tax_ids = [(4, account_tax_id, 0) for account_tax_id in account_tax_ids]
|
||||
TaxRepLine = self.env["account.tax.repartition.line"]
|
||||
tax_tag_ids = TaxRepLine.search(
|
||||
[
|
||||
("tax_id", "in", account_tax_ids),
|
||||
("repartition_type", "=", "base"),
|
||||
]
|
||||
).tag_ids
|
||||
|
||||
tax_repartition_line_id = False
|
||||
if salary_rule.account_tax_id:
|
||||
TaxRepLine = self.env["account.tax.repartition.line"]
|
||||
tax_repartition_line_id = TaxRepLine.search(
|
||||
[
|
||||
("document_type", "=", "invoice"),
|
||||
("tax_id", "=", salary_rule.account_tax_id.id),
|
||||
(
|
||||
"account_id",
|
||||
"=",
|
||||
salary_rule.account_debit.id or salary_rule.account_credit.id,
|
||||
),
|
||||
]
|
||||
).id
|
||||
tax_tag_ids += TaxRepLine.search(
|
||||
[
|
||||
("document_type", "=", "invoice"),
|
||||
("tax_id", "=", salary_rule.account_tax_id.id),
|
||||
("repartition_type", "=", "tax"),
|
||||
(
|
||||
"account_id",
|
||||
"=",
|
||||
salary_rule.account_debit.id or salary_rule.account_credit.id,
|
||||
),
|
||||
]
|
||||
).tag_ids
|
||||
|
||||
return tax_ids, tax_tag_ids or False, tax_repartition_line_id
|
||||
62
payroll_account/models/hr_payslip_line.py
Normal file
62
payroll_account/models/hr_payslip_line.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class HrPayslipLine(models.Model):
|
||||
_inherit = "hr.payslip.line"
|
||||
|
||||
def _get_partner_id(self, credit_account):
|
||||
"""
|
||||
Get partner_id of slip line to use in account_move_line
|
||||
|
||||
Partner is required only for specific account types that track external
|
||||
relationships:
|
||||
- asset_receivable: Employee loans, salary advances, overpayments to recover
|
||||
→ Partner source: Employee (work_contact_id or
|
||||
bank_account_id.partner_id)
|
||||
- liability_payable: Third-party payables (tax authorities, insurance
|
||||
companies) typically managed through contribution
|
||||
registers
|
||||
→ Partner source: Register partner
|
||||
(salary_rule_id.register_id.partner_id)
|
||||
- liability_current: Current liabilities to pay employee salaries, net pay
|
||||
accruals
|
||||
→ Partner source: Employee (work_contact_id or
|
||||
bank_account_id.partner_id)
|
||||
|
||||
For other account types (expense, income, asset_current, etc.), no partner
|
||||
is assigned as these represent internal accounting entries without external
|
||||
party relationships.
|
||||
"""
|
||||
# Determine which account we're dealing with
|
||||
acc_type = (
|
||||
self.salary_rule_id.account_credit.account_type
|
||||
if credit_account
|
||||
else self.salary_rule_id.account_debit.account_type
|
||||
)
|
||||
|
||||
if acc_type in ("asset_receivable", "liability_current"):
|
||||
# Employee-related accounts - always employee
|
||||
return (
|
||||
self.slip_id.employee_id.work_contact_id.id
|
||||
if self.slip_id.employee_id.work_contact_id
|
||||
else (
|
||||
self.slip_id.employee_id.bank_account_id.partner_id.id
|
||||
if self.slip_id.employee_id.bank_account_id
|
||||
else False
|
||||
)
|
||||
)
|
||||
|
||||
elif acc_type == "liability_payable":
|
||||
# Third-party payables - always register partner
|
||||
return (
|
||||
self.salary_rule_id.register_id.partner_id.id
|
||||
if self.salary_rule_id.register_id
|
||||
and self.salary_rule_id.register_id.partner_id
|
||||
else False
|
||||
)
|
||||
|
||||
# All other account types (expense, income, etc.)
|
||||
return False
|
||||
17
payroll_account/models/hr_payslip_run.py
Normal file
17
payroll_account/models/hr_payslip_run.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_inherit = "hr.payslip.run"
|
||||
|
||||
journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
"Salary Journal",
|
||||
required=True,
|
||||
default=lambda self: self.env["account.journal"].search(
|
||||
[("type", "=", "general")], limit=1
|
||||
),
|
||||
)
|
||||
25
payroll_account/models/hr_salary_rule.py
Normal file
25
payroll_account/models/hr_salary_rule.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrSalaryRule(models.Model):
|
||||
_inherit = "hr.salary.rule"
|
||||
|
||||
analytic_account_id = fields.Many2one("account.analytic.account")
|
||||
account_tax_id = fields.Many2one("account.tax", "Tax")
|
||||
account_debit = fields.Many2one(
|
||||
"account.account",
|
||||
"Debit Account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
company_dependent=True,
|
||||
)
|
||||
account_credit = fields.Many2one(
|
||||
"account.account",
|
||||
"Credit Account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
company_dependent=True,
|
||||
)
|
||||
tax_base_id = fields.Many2one("hr.salary.rule", "Base")
|
||||
tax_line_ids = fields.One2many("hr.salary.rule", "tax_base_id", string="Tax lines")
|
||||
Reference in New Issue
Block a user