Initial commit: l10n_ir_hr_payroll (Odoo 18)
This commit is contained in:
commit
345805fde8
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.so
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Odoo
|
||||
*.log
|
||||
*.pot
|
||||
*.mo
|
||||
.session
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Database / dumps
|
||||
*.sql
|
||||
*.dump
|
||||
|
||||
# Node / Assets
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
.env.*
|
||||
3
__init__.py
Normal file
3
__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
30
__manifest__.py
Normal file
30
__manifest__.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
"name": "Iran - Payroll",
|
||||
"countries": ["ir"],
|
||||
"category": "Human Resources/Payroll",
|
||||
"description": """
|
||||
Iran Payroll and End of Service rules.
|
||||
=======================================
|
||||
- Basic calculation
|
||||
- End of service calculation
|
||||
- Other inputs (overtime, salary attachments, etc.)
|
||||
- Social insurance calculation
|
||||
- End of service provisions
|
||||
- Tax break calculations and deductions
|
||||
- Master payroll export
|
||||
""",
|
||||
"depends": ["hr_payroll"],
|
||||
"auto_install": ["hr_payroll"],
|
||||
"data": [
|
||||
# "security/ir.model.access.csv",
|
||||
"data/hr_rule_parameter_data.xml",
|
||||
"data/hr_salary_rule_category_data.xml",
|
||||
"data/hr_payroll_structure_type_data.xml",
|
||||
"data/hr_payroll_structure_data.xml",
|
||||
"data/hr_salary_rule_data.xml",
|
||||
"views/hr_contract_views.xml",
|
||||
# "views/hr_payroll_master_report_views.xml",
|
||||
],
|
||||
"license": "OEEL-1",
|
||||
}
|
||||
14
data/hr_payroll_structure_data.xml
Normal file
14
data/hr_payroll_structure_data.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="hr_payroll_structure_ir_employee_salary" model="hr.payroll.structure">
|
||||
<field name="name">Iran: Regular Pay</field>
|
||||
<field name="code">IRMONTHLY</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
<field name="type_id" ref="structure_type_employee_ir"/>
|
||||
</record>
|
||||
<record id="l10n_ir_hr_payroll.structure_type_employee_ir" model="hr.payroll.structure.type">
|
||||
<field name="default_struct_id" ref="hr_payroll_structure_ir_employee_salary"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
10
data/hr_payroll_structure_type_data.xml
Normal file
10
data/hr_payroll_structure_type_data.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="structure_type_employee_ir" model="hr.payroll.structure.type">
|
||||
<field name="name">Iran: Employee</field>
|
||||
<field name="default_resource_calendar_id" ref="resource.resource_calendar_std"/>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
105
data/hr_rule_parameter_data.xml
Normal file
105
data/hr_rule_parameter_data.xml
Normal file
@ -0,0 +1,105 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Transport Allowance Daily Rate Parameter -->
|
||||
<record id="rule_parameter_transport_rate" model="hr.rule.parameter">
|
||||
<field name="name">Transport Allowance Per Day</field>
|
||||
<field name="code">transport_rate</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_value_transport_rate_1404" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">150000.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_transport_rate"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_housing_allowance" model="hr.rule.parameter">
|
||||
<field name="name">Housing Allowance</field>
|
||||
<field name="code">housing_allowance</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_housing_allowance" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">9000000.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_housing_allowance"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_consumption_allowance" model="hr.rule.parameter">
|
||||
<field name="name">Food Allowance</field>
|
||||
<field name="code">CONSUMPTION_ALLOWANCE</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_consumption_allowance" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">22000000.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_consumption_allowance"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_child_allowance" model="hr.rule.parameter">
|
||||
<field name="name">Child Allowance</field>
|
||||
<field name="code">child_allowance</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_child_allowance" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">933558.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_child_allowance"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_severance_pay" model="hr.rule.parameter">
|
||||
<field name="name">Daily Severance Pay</field>
|
||||
<field name="code">severance_pay</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_severance_pay" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">116193.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_severance_pay"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="rule_parameter_holiday_overtime_rate" model="hr.rule.parameter">
|
||||
<field name="name">Holiday Overtime Rate</field>
|
||||
<field name="code">holiday_overtime_rate</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_holiday_overtime_rate" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">180.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_holiday_overtime_rate"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_marital_allowance" model="hr.rule.parameter">
|
||||
<field name="name">Marital Allowance</field>
|
||||
<field name="code">marital_allowance</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_marital_allowance" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">5000000.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_marital_allowance"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_goodattenace_allowance" model="hr.rule.parameter">
|
||||
<field name="name">Good Attendance Allowance</field>
|
||||
<field name="code">GOODATTENDANCE_ALLOWANCE</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
<record id="rule_parameter_value_goodattenace_allowance" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">3000000.0</field>
|
||||
<field name="rule_parameter_id" ref="rule_parameter_goodattenace_allowance"/>
|
||||
<field name="date_from" eval="datetime(2025, 3, 20).date()"/>
|
||||
</record>
|
||||
<record id="rule_parameter_employee_insurance_rate" model="hr.rule.parameter">
|
||||
<field name="name">Employee Insurance Contribution (%)</field>
|
||||
<field name="code">employee_insurance_rate</field>
|
||||
<field name="country_id" ref="base.ir"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_parameter_value_employee_insurance_rate" model="hr.rule.parameter.value">
|
||||
<field name="parameter_value">7.0</field> <!-- 7% سهم کارمند -->
|
||||
<field name="rule_parameter_id" ref="rule_parameter_employee_insurance_rate"/>
|
||||
<field name="date_from" eval="datetime(2024, 3, 20).date()"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
43
data/hr_salary_rule_category_data.xml
Normal file
43
data/hr_salary_rule_category_data.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="hr_salary_rule_category_ua_unincluded" model="hr.salary.rule.category">
|
||||
<field name="name">UNINCLUDED ALW</field>
|
||||
<field name="code">UAWD</field>
|
||||
</record>
|
||||
<!-- Categories (hr.salary.rule.category) -->
|
||||
<record id="hr_salary_rule_category_ir_basic" model="hr.salary.rule.category">
|
||||
<field name="name">پایه</field>
|
||||
<field name="code">BASIC</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_allowance" model="hr.salary.rule.category">
|
||||
<field name="name">کمکهزینه</field>
|
||||
<field name="code">ALW</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_expense_settlement" model="hr.salary.rule.category">
|
||||
<field name="name">تسویه هزینهها</field>
|
||||
<field name="code">EXPENSE</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_gross" model="hr.salary.rule.category">
|
||||
<field name="name">ناخالص</field>
|
||||
<field name="code">GROSS</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_company_share" model="hr.salary.rule.category">
|
||||
<field name="name">سهم شرکت</field>
|
||||
<field name="code">COMP</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_deduction" model="hr.salary.rule.category">
|
||||
<field name="name">کسورات پرداخت</field>
|
||||
<field name="code">DED</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_category_ir_net" model="hr.salary.rule.category">
|
||||
<field name="name">خالص</field>
|
||||
<field name="code">NET</field>
|
||||
</record>
|
||||
<!-- Tax totals category (if needed) -->
|
||||
<record id="hr_salary_rule_category_ir_tax_totals" model="hr.salary.rule.category">
|
||||
<field name="name">Tax Totals</field>
|
||||
<field name="code">TAX</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
377
data/hr_salary_rule_data.xml
Normal file
377
data/hr_salary_rule_data.xml
Normal file
@ -0,0 +1,377 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="hr_salary_rule_ir_monthly_wage" model="hr.salary.rule">
|
||||
<field name="name">حقوق پایه</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">MONTHLY_WAGE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_basic"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
work_hours = worked_days['WORK100'].number_of_hours if 'WORK100' in worked_days else 0
|
||||
leave_120_hours = worked_days['LEAVE120'].number_of_hours if 'LEAVE120' in worked_days else 0
|
||||
leave_100_hours = worked_days['LEAVE100'].number_of_hours if 'LEAVE100' in worked_days else 0
|
||||
|
||||
total_hours = work_hours + leave_120_hours + leave_100_hours
|
||||
result = contract.wage * total_hours / 8
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_overtime_wage" model="hr.salary.rule">
|
||||
<field name="name">اضافهکاری</field>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="code">OVERTIME_WAGE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_basic"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = worked_days['OVERTIME'].number_of_hours</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = contract.wage * worked_days['OVERTIME'].number_of_hours * 1.4 / 8
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_housing_allowance" model="hr.salary.rule">
|
||||
<field name="name">کمک هزینه مسکن</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="code">HOUSING_ALLOWANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_allowance"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip._rule_parameter('housing_allowance')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_marital_allowance" model="hr.salary.rule">
|
||||
<field name="name">کمک هزینه عائلهمندی</field>
|
||||
<field name="sequence" eval="11"/>
|
||||
<field name="code">MARITAL_ALLOWANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_allowance"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = employee.marital == 'married'</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip._rule_parameter('marital_allowance')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_family_allowance" model="hr.salary.rule">
|
||||
<field name="name">کمک هزینه فرزند</field>
|
||||
<field name="sequence" eval="12"/>
|
||||
<field name="code">FAMILY_ALLOWANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_allowance"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = employee.children > 0</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip._rule_parameter('child_allowance') * employee.children
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_CONSUMPTION_ALLOWANCE" model="hr.salary.rule">
|
||||
<field name="name">کمک هزینه خواربار</field>
|
||||
<field name="sequence" eval="13"/>
|
||||
<field name="code">CONSUMPTION_ALLOWANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_allowance"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip._rule_parameter('CONSUMPTION_ALLOWANCE')</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip._rule_parameter('CONSUMPTION_ALLOWANCE')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_GOODATTENACE_ALLOWANCE" model="hr.salary.rule">
|
||||
<field name="name">حضور به موقع</field>
|
||||
<field name="sequence" eval="14"/>
|
||||
<field name="code">GOODATTENDANCE_ALLOWANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_allowance"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip._rule_parameter('GOODATTENDANCE_ALLOWANCE')</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip._rule_parameter('GOODATTENDANCE_ALLOWANCE')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_remained_expenses" model="hr.salary.rule">
|
||||
<field name="name">مانده هزینهها</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="code">REMAINED_EXPENSES</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_expense_settlement"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = inputs['REIMBURSEMENT']</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = inputs['REIMBURSEMENT']
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_ssp_company" model="hr.salary.rule">
|
||||
<field name="name">بیمه سهم کارفرما</field>
|
||||
<field name="sequence" eval="50"/>
|
||||
<field name="code">SSP_COMPANY</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_company_share"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
# مبنای مشمول بیمه: جمع BASIC و ALW
|
||||
insurable_base = categories['BASIC'] + categories['ALW']
|
||||
# سهم کارفرما ۲۳٪ و سهم کارمند ۷٪
|
||||
employer_share = insurable_base * 0.23
|
||||
# محاسبه ناخالص مشمول بیمه
|
||||
result = employer_share
|
||||
result_name = "ناخالص مشمول بیمه"
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_unemployment_security" model="hr.salary.rule">
|
||||
<field name="name">بیمه بیکاری</field>
|
||||
<field name="sequence" eval="51"/>
|
||||
<field name="code">UNEMPLOYMENT_SECURITY</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_company_share"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = 0.0
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_ssp_employee" model="hr.salary.rule">
|
||||
<field name="name">بیمه سهم کارگر</field>
|
||||
<field name="sequence" eval="52"/>
|
||||
<field name="code">SSP_EMPLOYEE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
# مبنای مشمول بیمه: جمع BASIC و ALW
|
||||
insurable_base = categories['BASIC'] + categories['ALW']
|
||||
# سهم کارفرما ۲۳٪ و سهم کارمند ۷٪
|
||||
employer_share = insurable_base * 0.23
|
||||
employee_share = insurable_base * 0.07
|
||||
# محاسبه ناخالص مشمول بیمه
|
||||
result = employee_share
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_gross_ssp" model="hr.salary.rule">
|
||||
<field name="name">ناخالص مشمول بیمه</field>
|
||||
<field name="sequence" eval="60"/>
|
||||
<field name="code">GROSS_SSP</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_gross"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
# مبنای مشمول بیمه: جمع BASIC و ALW
|
||||
insurable_base = categories['BASIC'] + categories['ALW']
|
||||
# سهم کارفرما ۲۳٪ و سهم کارمند ۷٪
|
||||
employer_share = insurable_base * 0.23
|
||||
employee_share = insurable_base * 0.07
|
||||
# محاسبه ناخالص مشمول بیمه
|
||||
result = insurable_base + employer_share + employee_share
|
||||
result_name = "ناخالص مشمول بیمه"
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_gross_taxable" model="hr.salary.rule">
|
||||
<field name="name">ناخالص مشمول مالیات</field>
|
||||
<field name="sequence" eval="61"/>
|
||||
<field name="code">GROSS_TAXABLE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_gross"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = worked_days['WORK100'].number_of_days</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
# ۱) مبلغ حقوق ماهانه
|
||||
mw = contract.wage * (worked_days['WORK100'].number_of_days + (worked_days['LEAVE120'].number_of_days if worked_days['LEAVE120'].number_of_days else 0.0) )
|
||||
|
||||
|
||||
# ۳) ناخالص مشمول مالیات = حقوق ماهانه + اضافهکاری
|
||||
result = mw
|
||||
# ۴) نام دلخواه برای نمایش
|
||||
result_name = "ناخالص مشمول مالیات"
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_tax_amount" model="hr.salary.rule">
|
||||
<field name="name">مالیات</field>
|
||||
<field name="sequence" eval="70"/>
|
||||
<field name="code">TAX_AMOUNT</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
monthly_wage = contract.wage
|
||||
|
||||
if monthly_wage <= 24000000:
|
||||
result = 0
|
||||
elif monthly_wage <= 30000000:
|
||||
result = (monthly_wage - 24000000) * 0.10
|
||||
elif monthly_wage <= 38000000:
|
||||
result = (6000000 * 0.10) + (monthly_wage - 30000000) * 0.15
|
||||
elif monthly_wage <= 50000000:
|
||||
result = (6000000 * 0.10) + (8000000 * 0.15) + (monthly_wage - 38000000) * 0.20
|
||||
elif monthly_wage <= 66667000:
|
||||
result = (6000000 * 0.10) + (8000000 * 0.15) + (12000000 * 0.20) + (monthly_wage - 50000000) * 0.25
|
||||
else:
|
||||
result = (6000000 * 0.10) + (8000000 * 0.15) + (12000000 * 0.20) + (16667000 * 0.25) + (monthly_wage - 66667000) * 0.30
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_supplementary_insurance" model="hr.salary.rule">
|
||||
<field name="name">بیمه تکمیلی</field>
|
||||
<field name="sequence" eval="71"/>
|
||||
<field name="code">SUPPLEMENTARY_INSURANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = inputs.ASSIG_SALARY</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = inputs['ASSIG_SALARY'].amount
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_loan_installment" model="hr.salary.rule">
|
||||
<field name="name">قسط وام</field>
|
||||
<field name="sequence" eval="72"/>
|
||||
<field name="code">LOAN_INSTALLMENT</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = inputs['DEDUCTION']</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = inputs['DEDUCTION'].amount
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_advance_installment" model="hr.salary.rule">
|
||||
<field name="name">قسط مساعده</field>
|
||||
<field name="sequence" eval="73"/>
|
||||
<field name="code">ADVANCE_INSTALLMENT</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = 0.0
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_delayed_checkin" model="hr.salary.rule">
|
||||
<field name="name">دیر آمدگی</field>
|
||||
<field name="sequence" eval="74"/>
|
||||
<field name="code">DELAYED_CHECKIN</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip.late_check_in_count_slip >= 2</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = 0
|
||||
result_name = ""
|
||||
if payslip.late_check_in_count_slip >= 2:
|
||||
result = inputs['DELAYED_CHCK'].amount
|
||||
result_name = "دیر آمدگی"
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ir_net" model="hr.salary.rule">
|
||||
<field name="name">خالص دریافتی</field>
|
||||
<field name="sequence" eval="100"/>
|
||||
<field name="code">NET</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_net"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = categories['BASIC'] + categories['ALW'] + categories['DED']
|
||||
</field>
|
||||
</record>
|
||||
<record id="iran_end_of_service_base_daily_1404" model="hr.salary.rule">
|
||||
<field name="name">End of Service Base Daily - 1404</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="code">EOSBASE1404_D</field>
|
||||
<field name="sequence">7</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
# جدول پایه سنوات روزانه به ازای سال سابقه
|
||||
service_table = {
|
||||
0: 0, 1: 94000, 2: 186400, 3: 299128, 4: 435529, 5: 561017, 6: 673958,
|
||||
7: 764873, 8: 839722, 9: 922359, 10: 976799, 11: 1038860, 12: 1075168,
|
||||
13: 1099568, 14: 1121934, 15: 1141081, 16: 1161373, 17: 1174944,
|
||||
18: 1189193, 19: 1204156, 20: 1220615, 21: 1235386, 22: 1248422,
|
||||
23: 1258705, 24: 1267521, 25: 1275000, 26: 1281327, 27: 1285196,
|
||||
28: 1288468, 29: 1291772, 30: 1295768, 31: 1299202
|
||||
}
|
||||
years = int(contract.l10n_ir_years_of_service or 0)
|
||||
daily_amount = service_table.get(years, 0)
|
||||
date_from = payslip.date_from
|
||||
date_to = payslip.date_to
|
||||
month_days = (date_to - date_from).days + 1
|
||||
result = daily_amount * month_days
|
||||
</field>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
</record>
|
||||
<record id="hr_salary_rule_ir_low_performance" model="hr.salary.rule">
|
||||
<field name="name">کمکارکرد</field>
|
||||
<field name="sequence" eval="76"/>
|
||||
<field name="code">LOW_PERFORMANCE</field>
|
||||
<field name="category_id" ref="hr_salary_rule_category_ir_deduction"/>
|
||||
<field name="struct_id" ref="l10n_ir_hr_payroll.hr_payroll_structure_ir_employee_salary"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">
|
||||
expected_hours = worked_days['WORK100'].number_of_hours if 'WORK100' in worked_days else 0.0
|
||||
actual_hours = (
|
||||
worked_days['WORK100'].number_of_hours if 'WORK100' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE120'].number_of_hours if 'LEAVE120' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE105'].number_of_hours if 'LEAVE105' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE110'].number_of_hours if 'LEAVE110' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['WORK110'].number_of_hours if 'WORK110' in worked_days else 0.0
|
||||
)
|
||||
result = actual_hours < expected_hours
|
||||
</field>
|
||||
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
expected_hours = worked_days['WORK100'].number_of_hours if 'WORK100' in worked_days else 0.0
|
||||
actual_hours = (
|
||||
worked_days['WORK100'].number_of_hours if 'WORK100' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE120'].number_of_hours if 'LEAVE120' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE105'].number_of_hours if 'LEAVE105' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['LEAVE110'].number_of_hours if 'LEAVE110' in worked_days else 0.0
|
||||
) + (
|
||||
worked_days['WORK110'].number_of_hours if 'WORK110' in worked_days else 0.0
|
||||
)
|
||||
short_hours = max(0.0, expected_hours - actual_hours)
|
||||
hourly_rate = contract.wage / 30.0 / (contract.resource_calendar_id.hours_per_day or 8.0)
|
||||
result = -1 * short_hours * hourly_rate
|
||||
result_name = "کمکارکرد"
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
9
models/__init__.py
Normal file
9
models/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_payslip
|
||||
from . import hr_payroll_dashboard
|
||||
from . import hr_payslip_worked_days
|
||||
from . import hr_payslip_run
|
||||
|
||||
|
||||
46
models/hr_contract.py
Normal file
46
models/hr_contract.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class HRContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
|
||||
l10n_ir_other_allowances = fields.Monetary(string='Iran Other Allowances')
|
||||
l10n_ir_responsibility_allowance = fields.Monetary(string='Iran Responsibility Allowance')
|
||||
|
||||
l10n_ir_years_of_service = fields.Integer(
|
||||
string='Years of Service',
|
||||
compute='_compute_years_of_service',
|
||||
inverse='_inverse_years_of_service',
|
||||
store=True,
|
||||
help='Sum of all completed service years for the employee across confirmed contracts.')
|
||||
|
||||
|
||||
_sql_constraints = [
|
||||
('check_l10n_ir_years_of_service_positive', 'CHECK(l10n_ir_years_of_service >= 0)',
|
||||
'Years of Service must be equal to or greater than 0'),
|
||||
]
|
||||
|
||||
def _inverse_years_of_service(self):
|
||||
# No action needed — just having this allows field to be editable manually
|
||||
# The manually set value will stay until a recompute is triggered
|
||||
pass
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_years_of_service(self):
|
||||
for contract in self:
|
||||
total_days = 0
|
||||
if contract.employee_id:
|
||||
employee_contracts = self.env['hr.contract'].search([
|
||||
('employee_id', '=', contract.employee_id.id),
|
||||
('state', 'in', ['open', 'close']),
|
||||
('date_start', '!=', False),
|
||||
('date_end', '!=', False),
|
||||
])
|
||||
for c in employee_contracts:
|
||||
duration = (c.date_end - c.date_start).days
|
||||
total_days += max(duration, 0)
|
||||
contract.l10n_ir_years_of_service = total_days // 365
|
||||
|
||||
303
models/hr_payroll_dashboard.py
Normal file
303
models/hr_payroll_dashboard.py
Normal file
@ -0,0 +1,303 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Jalali‐aware extensions for the Payroll Dashboard in Odoo 18."""
|
||||
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import jdatetime
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import format_amount
|
||||
|
||||
# --- Constants and mappings ------------------------------------------------
|
||||
|
||||
# Mapping of Persian month names (Gregorian) to month numbers
|
||||
GREG_MONTHS_FA = {
|
||||
'ژانویه': 1, 'ژانویهٔ': 1, 'فوریه': 2, 'مارس': 3,
|
||||
'آوریل': 4, 'مه': 5, 'مهٔ': 5, 'ژوئن': 6,
|
||||
'ژوئیه': 7, 'ژوئیهٔ': 7, 'اوت': 8, 'سپتامبر': 9,
|
||||
'اکتبر': 10, 'نوامبر': 11, 'دسامبر': 12,
|
||||
}
|
||||
|
||||
# Jalali month names (1–12)
|
||||
JALALI_MONTH_NAMES = [
|
||||
'فروردین', 'اردیبهشت', 'خرداد', 'تیر',
|
||||
'مرداد', 'شهریور', 'مهر', 'آبان',
|
||||
'آذر', 'دی', 'بهمن', 'اسفند',
|
||||
]
|
||||
|
||||
# Translation map for Persian digits to ASCII digits
|
||||
PERSIAN_DIGIT_MAP = {ord(ch): str(i) for i, ch in enumerate('۰۱۲۳۴۵۶۷۸۹')}
|
||||
|
||||
|
||||
def _to_int(string):
|
||||
"""
|
||||
Convert a string containing Persian or ASCII digits to an integer.
|
||||
"""
|
||||
try:
|
||||
return int(string.translate(PERSIAN_DIGIT_MAP))
|
||||
except ValueError:
|
||||
# Let any remaining errors bubble up
|
||||
return int(string)
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
"""Extend hr.payslip to support Jalali calendar in dashboard statistics."""
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
def _convert_label_to_jalali(self, label, period):
|
||||
"""
|
||||
Convert a Persian/Gregorian month‐year or ISO date label to Jalali format.
|
||||
|
||||
:param label: e.g. 'آوریل 2025', '2025-04-01', or '2025'
|
||||
:param period: 'monthly' to return 'فروردین 1404', otherwise '1404'
|
||||
:return: Jalali label or original on failure
|
||||
"""
|
||||
if not isinstance(label, str):
|
||||
return label
|
||||
|
||||
parts = label.strip().split()
|
||||
# Case 1: Persian month name + year
|
||||
if len(parts) >= 2:
|
||||
month_str, year_str = parts[0], parts[-1]
|
||||
try:
|
||||
year = _to_int(year_str)
|
||||
except ValueError:
|
||||
return label
|
||||
|
||||
month = GREG_MONTHS_FA.get(month_str)
|
||||
if month:
|
||||
greg_date = date(year, month, 1)
|
||||
else:
|
||||
try:
|
||||
greg_date = date.fromisoformat(label)
|
||||
except ValueError:
|
||||
return label
|
||||
|
||||
jal = jdatetime.date.fromgregorian(date=greg_date)
|
||||
if period == 'monthly':
|
||||
return f"{JALALI_MONTH_NAMES[jal.month - 1]} {jal.year}"
|
||||
return str(jal.year)
|
||||
|
||||
# Case 2: Year‐only or ISO full date
|
||||
try:
|
||||
year = _to_int(parts[0])
|
||||
greg_date = date(year, 12, 31)
|
||||
jal = jdatetime.date.fromgregorian(date=greg_date)
|
||||
return str(jal.year)
|
||||
except (ValueError, IndexError):
|
||||
try:
|
||||
greg_date = date.fromisoformat(label)
|
||||
return str(jdatetime.date.fromgregorian(date=greg_date).year)
|
||||
except ValueError:
|
||||
return label
|
||||
|
||||
@api.model
|
||||
def get_payroll_dashboard_data(self, sections=None):
|
||||
"""
|
||||
Override to convert batch dates to Jalali when the user lang is fa_IR.
|
||||
"""
|
||||
data = super().get_payroll_dashboard_data(sections)
|
||||
if (self.env.user.lang or self.env.lang) == 'fa_IR':
|
||||
for batch in data.get('batches', []):
|
||||
ds = batch.get('date_start')
|
||||
if not ds:
|
||||
continue
|
||||
|
||||
# Parse to Gregorian date
|
||||
if isinstance(ds, str):
|
||||
try:
|
||||
greg = fields.Date.from_string(ds)
|
||||
except ValueError:
|
||||
continue
|
||||
elif isinstance(ds, date):
|
||||
greg = ds
|
||||
else:
|
||||
continue
|
||||
|
||||
jal = jdatetime.date.fromgregorian(date=greg)
|
||||
batch['name'] = f"{JALALI_MONTH_NAMES[jal.month - 1]} {jal.year}"
|
||||
batch['date_start'] = fields.Date.to_string(
|
||||
date(jal.year, jal.month, jal.day)
|
||||
)
|
||||
return data
|
||||
|
||||
def _jalali_month_bounds(self, year, month):
|
||||
"""
|
||||
Compute Gregorian start and end dates for a given Jalali month.
|
||||
|
||||
:return: (start_date, end_date) in Gregorian calendar
|
||||
"""
|
||||
start_j = jdatetime.date(year, month, 1)
|
||||
start_g = start_j.togregorian()
|
||||
end_g = start_g + relativedelta(months=1) - relativedelta(days=1)
|
||||
return start_g, end_g
|
||||
|
||||
def _jalali_year_bounds(self, year):
|
||||
"""
|
||||
Compute Gregorian start and end dates for a given Jalali year.
|
||||
|
||||
:return: (start_date, end_date) in Gregorian calendar
|
||||
"""
|
||||
start_j = jdatetime.date(year, 1, 1)
|
||||
start_g = start_j.togregorian()
|
||||
end_g = start_g + relativedelta(years=1) - relativedelta(days=1)
|
||||
return start_g, end_g
|
||||
|
||||
@api.model
|
||||
def _get_dashboard_stats_employer_cost(self):
|
||||
"""
|
||||
Return employer cost stats in Jalali periods if lang is fa_*. Falls back otherwise.
|
||||
"""
|
||||
lang = (self.env.user.lang or '').lower()
|
||||
if not lang.startswith('fa'):
|
||||
return super()._get_dashboard_stats_employer_cost()
|
||||
|
||||
today = fields.Date.context_today(self)
|
||||
j_today = jdatetime.date.fromgregorian(date=today)
|
||||
cost_codes = self._get_dashboard_stat_employer_cost_codes()
|
||||
|
||||
result = {
|
||||
'type': 'stacked_bar',
|
||||
'title': _('Employer Cost'),
|
||||
'label': _('Employer Cost'),
|
||||
'id': 'employer_cost',
|
||||
'is_sample': False,
|
||||
'actions': [],
|
||||
'data': {
|
||||
'monthly': defaultdict(lambda: [{} for _ in range(3)]),
|
||||
'yearly': defaultdict(lambda: [{} for _ in range(3)]),
|
||||
},
|
||||
}
|
||||
|
||||
# Process both monthly and yearly in a loop to avoid duplication
|
||||
for period, bounds_func, labels in [
|
||||
('monthly', self._jalali_month_bounds, JALALI_MONTH_NAMES),
|
||||
('yearly', self._jalali_year_bounds, None),
|
||||
]:
|
||||
for idx, offset in enumerate([-2, -1, 0]):
|
||||
if period == 'monthly':
|
||||
m = j_today.month + offset
|
||||
y = j_today.year
|
||||
if m < 1:
|
||||
m += 12; y -= 1
|
||||
elif m > 12:
|
||||
m -= 12; y += 1
|
||||
start_g, end_g = bounds_func(y, m)
|
||||
label = f"{JALALI_MONTH_NAMES[m - 1]} {y}"
|
||||
else:
|
||||
y = j_today.year + offset
|
||||
start_g, end_g = bounds_func(y)
|
||||
label = str(y)
|
||||
|
||||
slips = self.env['hr.payslip'].search([
|
||||
('state', '!=', 'cancel'),
|
||||
('date_from', '>=', start_g),
|
||||
('date_to', '<=', end_g),
|
||||
])
|
||||
lines = slips._get_line_values(cost_codes.keys())
|
||||
|
||||
for code, desc in cost_codes.items():
|
||||
total = sum(lines[code][slip.id]['total'] for slip in slips)
|
||||
slot = result['data'][period][desc][idx]
|
||||
slot.update({
|
||||
'value': round(total, 2),
|
||||
'formatted_value': format_amount(
|
||||
self.env, total, self.env.company.currency_id
|
||||
),
|
||||
'label': label,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _get_dashboard_stat_employee_trends(self):
|
||||
"""
|
||||
Return employee‐count trends in Jalali monthly/yearly bars if lang is fa_*.
|
||||
Uses direct SQL for performance and falls back to samples if empty.
|
||||
"""
|
||||
lang = (self.env.user.lang or '').lower()
|
||||
if not lang.startswith('fa'):
|
||||
return super()._get_dashboard_stat_employee_trends()
|
||||
|
||||
today = fields.Date.context_today(self)
|
||||
j_today = jdatetime.date.fromgregorian(date=today)
|
||||
|
||||
# Build Jalali periods
|
||||
periods = []
|
||||
for period, shift in [('monthly', m) for m in (-1, 0, 1)] + [('yearly', y) for y in (-1, 0, 1)]:
|
||||
if period == 'monthly':
|
||||
m = j_today.month + shift
|
||||
y = j_today.year
|
||||
if m < 1:
|
||||
m += 12; y -= 1
|
||||
elif m > 12:
|
||||
m -= 12; y += 1
|
||||
start, end = self._jalali_month_bounds(y, m)
|
||||
else:
|
||||
y = j_today.year + shift
|
||||
start, end = self._jalali_year_bounds(y)
|
||||
tag = ('past', 'present', 'future')[shift + 1]
|
||||
periods.append((period, tag, start))
|
||||
|
||||
# SQL aggregation
|
||||
counts = {}
|
||||
for period, tag, start in periods:
|
||||
self.env.cr.execute("""
|
||||
SELECT COUNT(DISTINCT c.employee_id)
|
||||
FROM hr_contract c
|
||||
WHERE (c.date_end >= %s OR c.date_end IS NULL)
|
||||
AND c.date_start <= %s
|
||||
AND c.active = TRUE
|
||||
AND c.company_id IN %s
|
||||
""", (start, start, tuple(self.env.companies.ids)))
|
||||
counts[(period, tag)] = self.env.cr.fetchone()[0]
|
||||
|
||||
trends = {
|
||||
'type': 'bar',
|
||||
'title': _('Employee Trends'),
|
||||
'label': _('Employee Count'),
|
||||
'id': 'employees',
|
||||
'is_sample': False,
|
||||
'actions': self._get_employee_stats_actions(),
|
||||
'data': {'monthly': [{}, {}, {}], 'yearly': [{}, {}, {}]},
|
||||
}
|
||||
idx_map = {'past': 0, 'present': 1, 'future': 2}
|
||||
|
||||
# Populate real data
|
||||
for (period, tag), count in counts.items():
|
||||
idx = idx_map[tag]
|
||||
start = periods[idx_map[tag] if period=='monthly' else idx_map[tag]+3][2]
|
||||
jal = jdatetime.date.fromgregorian(date=start)
|
||||
label = (
|
||||
f"{JALALI_MONTH_NAMES[jal.month - 1]} {jal.year}"
|
||||
if period == 'monthly' else str(jal.year)
|
||||
)
|
||||
trends['data'][period][idx] = {
|
||||
'label': label, 'value': count, 'name': label
|
||||
}
|
||||
|
||||
# Sample fallback
|
||||
if not any(trends['data']['monthly']):
|
||||
trends['is_sample'] = True
|
||||
for period in ('monthly', 'yearly'):
|
||||
for i in range(3):
|
||||
if not trends['data'][period][i]:
|
||||
if period == 'monthly':
|
||||
m = j_today.month + (i - 1)
|
||||
y = j_today.year
|
||||
if m < 1:
|
||||
m += 12; y -= 1
|
||||
elif m > 12:
|
||||
m -= 12; y += 1
|
||||
label = f"{JALALI_MONTH_NAMES[m - 1]} {y}"
|
||||
else:
|
||||
label = str(j_today.year + (i - 1))
|
||||
trends['data'][period][i] = {
|
||||
'label': label,
|
||||
'value': random.randint(1000, 1500),
|
||||
'name': label,
|
||||
}
|
||||
|
||||
return trends
|
||||
136
models/hr_payroll_master_report.py
Normal file
136
models/hr_payroll_master_report.py
Normal file
@ -0,0 +1,136 @@
|
||||
import base64
|
||||
import io
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.tools.misc import xlsxwriter
|
||||
|
||||
XLSX = {
|
||||
"NUMBER": 0,
|
||||
"TEXT": 1,
|
||||
"DATE": 2,
|
||||
"FORMULA": 3,
|
||||
"LABEL": 4,
|
||||
}
|
||||
|
||||
|
||||
class HrirMasterReport(models.Model):
|
||||
_name = "report.l10n_ir_hr_payroll.master"
|
||||
_description = "Eygpt Master Payroll Report"
|
||||
|
||||
name = fields.Char(compute="_compute_name", store=True)
|
||||
date_from = fields.Date(required=True, default=fields.Date.today() + relativedelta(day=1))
|
||||
date_to = fields.Date(required=True, default=fields.Date.today() + relativedelta(day=1, months=1, days=-1))
|
||||
xlsx_file = fields.Binary(string="Report", readonly=True)
|
||||
xlsx_filename = fields.Char(readonly=True)
|
||||
period_has_payslips = fields.Boolean(compute="_compute_period_has_payslips")
|
||||
|
||||
@api.model
|
||||
def default_get(self, field_list=None):
|
||||
if self.env.company.country_id.code != "ir":
|
||||
raise UserError(_("You must be logged in an iran company to use this feature"))
|
||||
return super().default_get(field_list)
|
||||
|
||||
@api.depends("date_from", "date_to")
|
||||
def _compute_name(self):
|
||||
for report in self:
|
||||
report.name = _(
|
||||
"Master Report %(date_from)s - %(date_to)s", date_from=report.date_from, date_to=report.date_to
|
||||
)
|
||||
|
||||
@api.depends("date_from", "date_to")
|
||||
def _compute_period_has_payslips(self):
|
||||
for report in self:
|
||||
payslips = report.env["hr.payslip"].search(
|
||||
[
|
||||
("date_from", ">=", report.date_from),
|
||||
("date_to", "<=", report.date_to),
|
||||
("company_id", "=", report.env.company.id),
|
||||
("state", "in", ["done", "paid"]),
|
||||
]
|
||||
)
|
||||
report.period_has_payslips = bool(payslips)
|
||||
|
||||
@api.constrains("date_from", "date_to")
|
||||
def _check_dates(self):
|
||||
for report in self:
|
||||
if report.date_from > report.date_to:
|
||||
raise ValidationError(_("The starting date must be before or equal to the ending date"))
|
||||
|
||||
@api.model
|
||||
def _write_row(self, worksheet, row_index, row, formats):
|
||||
for i, (formatting, *value) in enumerate(row):
|
||||
if formatting == XLSX["TEXT"]:
|
||||
worksheet.write(row_index, i, *value, formats[XLSX["TEXT"]])
|
||||
elif formatting == XLSX["DATE"]:
|
||||
worksheet.write_datetime(row_index, i, *value, formats[XLSX["DATE"]])
|
||||
|
||||
def action_generate_report(self):
|
||||
company = self.env.company
|
||||
if company.country_code != "ir":
|
||||
raise UserError(_("You must be logged in an iran company to use this feature"))
|
||||
|
||||
labels = [_("Employee ID"), _("Employee Name"), _("Joining Date"), _("Department"), _("Job Designation")]
|
||||
|
||||
output = io.BytesIO()
|
||||
workbook = xlsxwriter.Workbook(output)
|
||||
|
||||
formats = {
|
||||
XLSX["TEXT"]: workbook.add_format({"border": 1}),
|
||||
XLSX["DATE"]: workbook.add_format({"border": 1, "num_format": "dd/mm/yyyy"}),
|
||||
XLSX["LABEL"]: workbook.add_format({"border": 1, "bold": True}),
|
||||
}
|
||||
|
||||
payslips = self.env["hr.payslip"].search(
|
||||
[
|
||||
("date_from", ">=", self.date_from),
|
||||
("date_to", "<=", self.date_to),
|
||||
("company_id", "=", company.id),
|
||||
("state", "in", ["done", "paid"]),
|
||||
]
|
||||
)
|
||||
if not payslips:
|
||||
raise ValidationError(_("There are no eligible payslips for that period of time"))
|
||||
payslips_data = defaultdict(dict)
|
||||
for payslip in payslips:
|
||||
payslips_data[payslip.struct_id][payslip.employee_id] = payslip
|
||||
|
||||
for struct in payslips_data:
|
||||
worksheet = workbook.add_worksheet(name=struct.name)
|
||||
|
||||
i = 1
|
||||
for employee in payslips_data[struct]:
|
||||
joining_date = ""
|
||||
if employee.contract_id.date_start:
|
||||
joining_date = datetime.strptime(
|
||||
employee.contract_id.date_start.strftime("%Y-%m-%d"), "%Y-%m-%d"
|
||||
).date()
|
||||
|
||||
row = [
|
||||
(XLSX["TEXT"], employee.id),
|
||||
(XLSX["TEXT"], employee.name),
|
||||
(XLSX["DATE"], joining_date) if joining_date else (XLSX["TEXT"], ""),
|
||||
(XLSX["TEXT"], employee.department_id.name or ""),
|
||||
(XLSX["TEXT"], employee.job_title or ""),
|
||||
]
|
||||
if i == 1:
|
||||
labels.extend(payslips_data[struct][employee].line_ids.mapped("name"))
|
||||
row.extend(
|
||||
(XLSX["TEXT"], company.currency_id.format(t))
|
||||
for t in payslips_data[struct][employee].line_ids.mapped("total")
|
||||
)
|
||||
self._write_row(worksheet, i, row, formats)
|
||||
i += 1
|
||||
|
||||
for col, label in enumerate(labels):
|
||||
worksheet.write(0, col, label, formats[XLSX["LABEL"]])
|
||||
worksheet.set_column(0, len(labels) - 1, 20)
|
||||
|
||||
workbook.close()
|
||||
xlsx_data = output.getvalue()
|
||||
self.xlsx_file = base64.encodebytes(xlsx_data)
|
||||
self.xlsx_filename = f"{self.name}.xlsx"
|
||||
330
models/hr_payslip.py
Normal file
330
models/hr_payslip.py
Normal file
@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from collections import defaultdict
|
||||
|
||||
import jdatetime
|
||||
|
||||
from odoo import models, api, _
|
||||
from odoo.fields import Date
|
||||
from odoo.tools import date_utils
|
||||
|
||||
JALALI_MONTHS_FA = {
|
||||
1: "فروردین", 2: "اردیبهشت", 3: "خرداد", 4: "تیر",
|
||||
5: "مرداد", 6: "شهریور", 7: "مهر", 8: "آبان",
|
||||
9: "آذر", 10: "دی", 11: "بهمن", 12: "اسفند",
|
||||
}
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
def _get_period_name(self, cache):
|
||||
"""
|
||||
Return the display name for the payslip period.
|
||||
For Persian users (fa_IR), format the period using the Jalali calendar
|
||||
and custom month names; otherwise, use the superclass implementation.
|
||||
"""
|
||||
self.ensure_one()
|
||||
lang = self.employee_id.lang or self.env.user.lang
|
||||
if lang != 'fa_IR':
|
||||
return super(type(self), self)._get_period_name(cache)
|
||||
|
||||
start = jdatetime.date.fromgregorian(date=self.date_from)
|
||||
end = jdatetime.date.fromgregorian(date=self.date_to)
|
||||
schedule = (
|
||||
self.contract_id.schedule_pay
|
||||
or self.contract_id.structure_type_id.default_schedule_pay
|
||||
)
|
||||
|
||||
if schedule == 'monthly':
|
||||
return f"{JALALI_MONTHS_FA[start.month]} {start.year}"
|
||||
if schedule == 'quarterly':
|
||||
quarter = (start.month - 1) // 3 + 1
|
||||
return f"سهماهه {quarter} سال {start.year}"
|
||||
if schedule == 'semi-annually':
|
||||
half = 'اول' if start.month <= 6 else 'دوم'
|
||||
return f"نیمه {half} سال {start.year}"
|
||||
if schedule == 'annually':
|
||||
return f"سال {start.year}"
|
||||
if schedule == 'weekly':
|
||||
week = int(start.strftime('%U'))
|
||||
return f"هفته {week} سال {start.year}"
|
||||
if schedule == 'bi-weekly':
|
||||
week = int(start.strftime('%U'))
|
||||
return f"هفتههای {week} و {week + 1} سال {start.year}"
|
||||
if schedule == 'bi-monthly':
|
||||
return f"{JALALI_MONTHS_FA[start.month]} و {JALALI_MONTHS_FA[end.month]} {start.year}"
|
||||
# Fallback to explicit date range
|
||||
return f"{start} - {end}"
|
||||
|
||||
def _get_schedule_timedelta(self):
|
||||
"""
|
||||
Return a relativedelta between date_from and date_to.
|
||||
For Persian users, compute the end of the period in the Jalali calendar,
|
||||
convert to Gregorian, and return the difference; otherwise, defer
|
||||
to the superclass implementation.
|
||||
"""
|
||||
self.ensure_one()
|
||||
schedule = (
|
||||
self.contract_id.schedule_pay
|
||||
or self.contract_id.structure_type_id.default_schedule_pay
|
||||
)
|
||||
lang = self.employee_id.lang or self.env.user.lang
|
||||
if lang != 'fa_IR':
|
||||
return super(type(self), self)._get_schedule_timedelta()
|
||||
|
||||
# Convert the start date to Jalali
|
||||
j_start = jdatetime.date.fromgregorian(date=self.date_from)
|
||||
|
||||
# Determine the Jalali end date based on schedule
|
||||
if schedule == 'monthly':
|
||||
# Next month first day minus one day
|
||||
if j_start.month == 12:
|
||||
ny, nm = j_start.year + 1, 1
|
||||
else:
|
||||
ny, nm = j_start.year, j_start.month + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'quarterly':
|
||||
nm = ((j_start.month - 1) // 3 + 1) * 3 + 1
|
||||
ny = j_start.year + (nm - 1) // 12
|
||||
nm = ((nm - 1) % 12) + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'semi-annually':
|
||||
nm = j_start.month + 6
|
||||
ny = j_start.year + (nm - 1) // 12
|
||||
nm = ((nm - 1) % 12) + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'annually':
|
||||
j_first_next = jdatetime.date(j_start.year + 1, 1, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'weekly':
|
||||
j_end = j_start + jdatetime.timedelta(days=6)
|
||||
elif schedule == 'bi-weekly':
|
||||
j_end = j_start + jdatetime.timedelta(days=13)
|
||||
elif schedule == 'bi-monthly':
|
||||
nm = j_start.month + 2
|
||||
ny = j_start.year + (nm - 1) // 12
|
||||
nm = ((nm - 1) % 12) + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
else:
|
||||
# Default monthly fallback
|
||||
if j_start.month == 12:
|
||||
ny, nm = j_start.year + 1, 1
|
||||
else:
|
||||
ny, nm = j_start.year, j_start.month + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end = j_first_next - jdatetime.timedelta(days=1)
|
||||
|
||||
# Convert back to Gregorian and compute delta
|
||||
g_start = j_start.togregorian()
|
||||
g_end = j_end.togregorian()
|
||||
return relativedelta(g_end, g_start)
|
||||
|
||||
def _get_schedule_period_start(self):
|
||||
"""
|
||||
Compute the start date of the payslip period.
|
||||
For Persian users, determine the first day of the period in the Jalali calendar
|
||||
then convert to Gregorian; otherwise, defer to the superclass.
|
||||
"""
|
||||
self.ensure_one()
|
||||
schedule = (
|
||||
self.contract_id.schedule_pay
|
||||
or self.contract_id.structure_type_id.default_schedule_pay
|
||||
)
|
||||
lang = self.env.user.lang or 'en_US'
|
||||
today = date.today()
|
||||
|
||||
if lang == 'fa_IR':
|
||||
j_today = jdatetime.date.fromgregorian(date=today)
|
||||
if schedule == 'quarterly':
|
||||
quarter = (j_today.month - 1) // 3
|
||||
start_month = quarter * 3 + 1
|
||||
j_start = jdatetime.date(j_today.year, start_month, 1)
|
||||
elif schedule == 'semi-annually':
|
||||
sm = 7 if j_today.month > 6 else 1
|
||||
j_start = jdatetime.date(j_today.year, sm, 1)
|
||||
elif schedule == 'annually':
|
||||
j_start = jdatetime.date(j_today.year, 1, 1)
|
||||
elif schedule == 'weekly':
|
||||
j_start = j_today - jdatetime.timedelta(days=j_today.weekday())
|
||||
elif schedule == 'bi-weekly':
|
||||
wk = int(j_today.strftime('%U'))
|
||||
j_start = j_today - jdatetime.timedelta(days=j_today.weekday() + 7 * (wk % 2 == 0))
|
||||
elif schedule == 'bi-monthly':
|
||||
start_month = ((j_today.month - 1) // 2) * 2 + 1
|
||||
j_start = jdatetime.date(j_today.year, start_month, 1)
|
||||
else:
|
||||
j_start = jdatetime.date(j_today.year, j_today.month, 1)
|
||||
|
||||
g_start = j_start.togregorian()
|
||||
if self.contract_id and g_start < self.contract_id.date_start:
|
||||
return self.contract_id.date_start
|
||||
return g_start
|
||||
|
||||
return super(type(self), self)._get_schedule_period_start()
|
||||
|
||||
@api.depends('date_from', 'date_to', 'struct_id')
|
||||
def _compute_warning_message(self):
|
||||
"""
|
||||
Compute warning messages for the payslip:
|
||||
1. Contract validity range
|
||||
2. Future date range beyond end of month
|
||||
3. Duration mismatch according to schedule
|
||||
Provides Jalali-aware checks and messages for Persian users.
|
||||
"""
|
||||
for slip in self.filtered(lambda p: p.date_to):
|
||||
slip.warning_message = False
|
||||
warnings = []
|
||||
lang = slip.employee_id.lang or slip.env.user.lang
|
||||
|
||||
# 1. Contract validity
|
||||
if slip.contract_id and (
|
||||
slip.date_from < slip.contract_id.date_start or
|
||||
(slip.contract_id.date_end and slip.date_to > slip.contract_id.date_end)
|
||||
):
|
||||
warnings.append(_("The period selected does not match the contract validity period."))
|
||||
|
||||
# 2. Future range warning
|
||||
if lang == 'fa_IR':
|
||||
j_today = jdatetime.date.fromgregorian(date=Date.today())
|
||||
if j_today.month == 12:
|
||||
ny, nm = j_today.year + 1, 1
|
||||
else:
|
||||
ny, nm = j_today.year, j_today.month + 1
|
||||
j_first_next = jdatetime.date(ny, nm, 1)
|
||||
j_end_of_month = j_first_next - jdatetime.timedelta(days=1)
|
||||
g_end_of_month = j_end_of_month.togregorian()
|
||||
start_str = f"{j_first_next.year}/{j_first_next.month:02}/{j_first_next.day:02}"
|
||||
end_str = f"{j_end_of_month.year}/{j_end_of_month.month:02}/{j_end_of_month.day:02}"
|
||||
if slip.date_to > g_end_of_month:
|
||||
warnings.append(_(
|
||||
"Work entries may not be generated for the period from %(start)s to %(end)s.",
|
||||
start=start_str, end=end_str
|
||||
))
|
||||
else:
|
||||
g_end_of_month = date_utils.end_of(Date.today(), 'month')
|
||||
g_start_next = date_utils.add(g_end_of_month, days=1)
|
||||
start_str = g_start_next
|
||||
end_str = slip.date_to
|
||||
if slip.date_to > g_end_of_month:
|
||||
warnings.append(_(
|
||||
"Work entries may not be generated for the period from %(start)s to %(end)s.",
|
||||
start=start_str, end=end_str
|
||||
))
|
||||
|
||||
# 3. Duration mismatch
|
||||
schedule = (
|
||||
slip.contract_id.schedule_pay
|
||||
or slip.contract_id.structure_type_id.default_schedule_pay
|
||||
)
|
||||
if schedule and lang == 'fa_IR':
|
||||
jd_from = jdatetime.date.fromgregorian(date=slip.date_from)
|
||||
jd_to = jdatetime.date.fromgregorian(date=slip.date_to)
|
||||
# Calculate expected end of Jalali period
|
||||
if schedule == 'monthly':
|
||||
if jd_from.month == 12:
|
||||
ny, nm = jd_from.year + 1, 1
|
||||
else:
|
||||
ny, nm = jd_from.year, jd_from.month + 1
|
||||
jfn = jdatetime.date(ny, nm, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'quarterly':
|
||||
nm_val = jd_from.month + 3
|
||||
ny_val = jd_from.year + (nm_val - 1) // 12
|
||||
nm_val = ((nm_val - 1) % 12) + 1
|
||||
jfn = jdatetime.date(ny_val, nm_val, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'semi-annually':
|
||||
nm_val = jd_from.month + 6
|
||||
ny_val = jd_from.year + (nm_val - 1) // 12
|
||||
nm_val = ((nm_val - 1) % 12) + 1
|
||||
jfn = jdatetime.date(ny_val, nm_val, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'annually':
|
||||
jfn = jdatetime.date(jd_from.year + 1, 1, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
elif schedule == 'weekly':
|
||||
expected = jd_from + jdatetime.timedelta(days=6)
|
||||
elif schedule == 'bi-weekly':
|
||||
expected = jd_from + jdatetime.timedelta(days=13)
|
||||
elif schedule == 'bi-monthly':
|
||||
nm_val = jd_from.month + 2
|
||||
ny_val = jd_from.year + (nm_val - 1) // 12
|
||||
nm_val = ((nm_val - 1) % 12) + 1
|
||||
jfn = jdatetime.date(ny_val, nm_val, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
else:
|
||||
if jd_from.month == 12:
|
||||
ny, nm = jd_from.year + 1, 1
|
||||
else:
|
||||
ny, nm = jd_from.year, jd_from.month + 1
|
||||
jfn = jdatetime.date(ny, nm, 1)
|
||||
expected = jfn - jdatetime.timedelta(days=1)
|
||||
if jd_to != expected:
|
||||
warnings.append(_("The duration of the payslip is not accurate according to the structure type."))
|
||||
else:
|
||||
if slip.date_from + slip._get_schedule_timedelta() != slip.date_to:
|
||||
warnings.append(_("The duration of the payslip is not accurate according to the structure type."))
|
||||
|
||||
# Finalize warning message
|
||||
if warnings:
|
||||
slip.warning_message = "\n ・ ".join([_("This payslip can be erroneous :")] + warnings)
|
||||
|
||||
def _get_transport_worked_day_lines_from_work_entries(self):
|
||||
"""
|
||||
Generate transport allowance worked day lines based on employee attendance entries.
|
||||
Counts days where worked hours >= 90% of expected hours, then creates a line entry.
|
||||
"""
|
||||
transport_lines = []
|
||||
attendance_type = self.env.ref('hr_work_entry.work_entry_type_attendance', raise_if_not_found=False)
|
||||
transport_type = self.env['hr.work.entry.type'].search([('code', '=', 'TRANSPORT')], limit=1)
|
||||
if not attendance_type or not transport_type:
|
||||
return []
|
||||
for slip in self:
|
||||
calendar = slip.contract_id.resource_calendar_id
|
||||
if not calendar:
|
||||
continue
|
||||
# Map expected hours per weekday
|
||||
expected = defaultdict(float)
|
||||
for att in calendar.attendance_ids:
|
||||
expected[att.dayofweek] += att.hour_to - att.hour_from
|
||||
# Sum actual worked hours per day
|
||||
entries = self.env['hr.work.entry'].search([
|
||||
('employee_id', '=', slip.employee_id.id),
|
||||
('work_entry_type_id', '=', attendance_type.id),
|
||||
('date_start', '>=', slip.date_from),
|
||||
('date_stop', '<=', slip.date_to),
|
||||
])
|
||||
worked = defaultdict(float)
|
||||
for entry in entries:
|
||||
day = entry.date_start.date()
|
||||
worked[day] += (entry.date_stop - entry.date_start).total_seconds() / 3600
|
||||
# Count days meeting threshold
|
||||
count = sum(1 for d, hrs in worked.items() if hrs >= 0.5 * expected[d.weekday()])
|
||||
if count:
|
||||
transport_lines.append((0, 0, {
|
||||
'name': _('ایاب و ذهاب و نهار'),
|
||||
'sequence': 90,
|
||||
'code': 'TRANSPORT',
|
||||
'work_entry_type_id': transport_type.id,
|
||||
'number_of_days': count,
|
||||
'number_of_hours': count * 8.0,
|
||||
'amount': 0.0,
|
||||
'is_paid': True,
|
||||
}))
|
||||
return transport_lines
|
||||
|
||||
def _get_new_worked_days_lines(self):
|
||||
"""
|
||||
Extend base worked day lines with transport allowance lines.
|
||||
"""
|
||||
base = super()._get_new_worked_days_lines()
|
||||
transport = self._get_transport_worked_day_lines_from_work_entries()
|
||||
return base + transport
|
||||
69
models/hr_payslip_run.py
Normal file
69
models/hr_payslip_run.py
Normal file
@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Jalali-aware inheritance for hr.payslip.run defaults
|
||||
|
||||
import jdatetime
|
||||
from datetime import date, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_inherit = 'hr.payslip.run'
|
||||
|
||||
# Redefine fields with Jalali-aware defaults
|
||||
date_start = fields.Date(
|
||||
string='Date From', required=True,
|
||||
default=lambda self: self._default_date_start()
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string='Date To', required=True,
|
||||
default=lambda self: self._default_date_end()
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_date_start(self):
|
||||
"""
|
||||
Compute the first day of the current month.
|
||||
Uses Jalali calendar for Persian users (fa_IR), otherwise Gregorian.
|
||||
Returns an ISO-formatted date string.
|
||||
"""
|
||||
lang = self.env.user.lang or 'en_US'
|
||||
if lang == 'fa_IR':
|
||||
j_today = jdatetime.date.fromgregorian(date=date.today())
|
||||
j_start = jdatetime.date(j_today.year, j_today.month, 1)
|
||||
return fields.Date.to_string(j_start.togregorian())
|
||||
return fields.Date.to_string(date.today().replace(day=1))
|
||||
|
||||
@api.model
|
||||
def _default_date_end(self):
|
||||
"""
|
||||
Compute the last day of the current month.
|
||||
Uses Jalali calendar for Persian users (fa_IR), otherwise Gregorian.
|
||||
Returns an ISO-formatted date string.
|
||||
"""
|
||||
lang = self.env.user.lang or 'en_US'
|
||||
if lang == 'fa_IR':
|
||||
j_today = jdatetime.date.fromgregorian(date=date.today())
|
||||
if j_today.month == 12:
|
||||
j_next = jdatetime.date(j_today.year + 1, 1, 1)
|
||||
else:
|
||||
j_next = jdatetime.date(j_today.year, j_today.month + 1, 1)
|
||||
j_end = j_next - jdatetime.timedelta(days=1)
|
||||
return fields.Date.to_string(j_end.togregorian())
|
||||
# Gregorian fallback: last day of this month
|
||||
end_date = (datetime.now() + relativedelta(months=1, day=1, days=-1)).date()
|
||||
return fields.Date.to_string(end_date)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""
|
||||
Override default_get to inject Jalali-aware defaults
|
||||
for date_start and date_end on record creation.
|
||||
"""
|
||||
res = super(HrPayslipRun, self).default_get(fields_list)
|
||||
if 'date_start' in fields_list:
|
||||
res['date_start'] = self._default_date_start()
|
||||
if 'date_end' in fields_list:
|
||||
res['date_end'] = self._default_date_end()
|
||||
return res
|
||||
16
models/hr_payslip_worked_days.py
Normal file
16
models/hr_payslip_worked_days.py
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class HrPayslipWorkedDays(models.Model):
|
||||
_inherit = 'hr.payslip.worked_days'
|
||||
|
||||
@api.depends('is_paid', 'is_credit_time', 'number_of_hours', 'payslip_id', 'contract_id.wage', 'payslip_id.sum_worked_hours')
|
||||
def _compute_amount(self):
|
||||
super()._compute_amount()
|
||||
for worked_days in self:
|
||||
if worked_days.code == 'TRANSPORT' and worked_days.is_paid:
|
||||
rate = self.env['hr.rule.parameter']._get_parameter_from_code('transport_rate', worked_days.payslip_id.date_to)
|
||||
worked_days.amount = worked_days.number_of_days * rate
|
||||
45
views/hr_contract_views.xml
Normal file
45
views/hr_contract_views.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_contract_form_inherit_ir" model="ir.ui.view">
|
||||
<field name="name">hr.contract.form.inherit.ir</field>
|
||||
<field name="model">hr.contract</field>
|
||||
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="salary" position="inside">
|
||||
<field name="country_code" invisible="1"/>
|
||||
|
||||
<label for="l10n_ir_other_allowances" invisible="country_code != 'IR'" string="Other Allowances"/>
|
||||
<div class="o_row mw-50" name="l10n_ir_other_allowances" invisible="country_code != 'IR'">
|
||||
<field name="l10n_ir_other_allowances" class="oe_inline o_hr_narrow_field" nolabel="1"/>
|
||||
<span>/ month</span>
|
||||
</div>
|
||||
|
||||
<label for="l10n_ir_responsibility_allowance" invisible="country_code != 'IR'"
|
||||
string="Responsibility Allowance"/>
|
||||
<div class="o_row mw-50" name="l10n_ir_responsibility_allowance" invisible="country_code != 'IR'">
|
||||
<field name="l10n_ir_responsibility_allowance" class="oe_inline o_hr_narrow_field" nolabel="1"/>
|
||||
<span>/ month</span>
|
||||
</div>
|
||||
</group>
|
||||
|
||||
<group name="yearly_benefits" position="after">
|
||||
<group name="ir_end_of_service" string="End of Service Benefit" invisible="country_code != 'IR'">
|
||||
<field name="l10n_ir_years_of_service"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Inherit the existing form view to change the placeholder -->
|
||||
<record id="hr_payslip_run_form_jalali" model="ir.ui.view">
|
||||
<field name="name">hr.payslip.run.form.jalali</field>
|
||||
<field name="model">hr.payslip.run</field>
|
||||
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Change the placeholder on the Batch Name field -->
|
||||
<xpath expr="//field[@name='name']" position="attributes">
|
||||
<attribute name="placeholder">مثال: فروردین ۱۴۰۴</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user