chore: import enterprise addons 19.0 bulk snapshot

This commit is contained in:
1b-agent committed 2026-09-16 10:13:08 +00:00
commit ca4af030d3
39951 files changed
+15570005

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import report_payroll_advice
from . import report_hr_yearly_salary_detail
from . import report_hr_epf
from . import report_hr_esic
+170
View File
@@ -0,0 +1,170 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import calendar
import io
from odoo import api, fields, models, _
from odoo.exceptions import UserError
MONTH_SELECTION = [
('1', 'January'),
('2', 'February'),
('3', 'March'),
('4', 'April'),
('5', 'May'),
('6', 'June'),
('7', 'July'),
('8', 'August'),
('9', 'September'),
('10', 'October'),
('11', 'November'),
('12', 'December'),
]
class L10nInHrPayrollEpfReport(models.Model):
_name = 'l10n.in.hr.payroll.epf.report'
_description = 'Indian Payroll: Employee Provident Fund Report'
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
month = fields.Selection(
MONTH_SELECTION,
required=True,
default=lambda self: str(fields.Date.context_today(self).month)
)
year = fields.Integer(required=True, default=lambda self: fields.Date.context_today(self).year)
xls_file = fields.Binary(string="XLS file")
xls_filename = fields.Char()
_unique_epf_report_per_month_year = models.Constraint(
'UNIQUE(company_id, month, year)',
"An EPF Report for this month and year already exists.",
)
@api.model
def default_get(self, fields):
if self.env.company.country_id.code != "IN":
raise UserError(_('You must be logged in a Indian company to use this feature'))
return super().default_get(fields)
@api.depends('month', 'year')
def _compute_display_name(self):
month_description = dict(self._fields['month']._description_selection(self.env))
for report in self:
report.display_name = f"{month_description.get(report.month)}-{report.year}"
def _get_employee_pf_data(self):
self.ensure_one()
# Get the relevant records based on the year and month
indian_employees = self.env['hr.employee'].search([
('version_id.l10n_in_provident_fund', '=', True),
('company_id', '=', self.company_id.id)
]).filtered(lambda e: e.company_country_code == 'IN')
result = []
end_date = calendar.monthrange(self.year, int(self.month))[1]
payslips = self.env['hr.payslip'].search([
('employee_id', 'in', indian_employees.ids),
('date_from', '>=', f'{self.year}-{int(self.month):02d}-01'),
('date_to', '<=', f'{self.year}-{int(self.month):02d}-{end_date:02d}'),
('state', 'in', ('validated', 'paid'))
])
if not payslips:
return []
payslip_line_values = payslips._get_line_values(['GROSS', 'BASIC', 'PF'])
for employee in indian_employees:
wage = 0
epf = 0
eps = 0
epf_contri = 0
payslip_ids = payslips.filtered(lambda p: p.employee_id == employee)
if not payslip_ids:
continue
for payslip in payslip_ids:
pf_value = payslip_line_values['PF'][payslip.id]['total']
if pf_value == 0:
continue
epf_contri -= pf_value
wage += payslip_line_values['GROSS'][payslip.id]['total']
epf += payslip_line_values['BASIC'][payslip.id]['total']
# Skip the employee if there are no valid PF contributions
if epf_contri == 0:
continue
# Calculate contributions and differences
eps = min(payslip_ids[0]._rule_parameter('l10n_in_pf_amount'), epf)
eps_contri = round(eps * payslip_ids[0]._rule_parameter('l10n_in_eps_contri_percent'), 2)
diff = round(epf_contri - eps_contri, 2)
result.append((
employee.l10n_in_uan or '',
employee.name,
wage,
epf,
eps,
eps,
epf_contri,
eps_contri,
diff,
0, 0,
))
return result
def action_export_xlsx(self):
self.ensure_one()
output = io.BytesIO()
import xlsxwriter # noqa: PLC0415
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
worksheet = workbook.add_worksheet('Employee_provident_fund_report')
style_highlight = workbook.add_format({'bold': True, 'pattern': 1, 'bg_color': '#E0E0E0', 'align': 'center'})
style_normal = workbook.add_format({'font_size': 12})
row = 0
worksheet.set_row(row, 20)
headers = [
"UAN",
"MEMBER NAME",
"GROSS WAGES",
"EPF WAGES",
"EPS WAGES",
"EDLI WAGES",
"EPF CONTRIBUTION REMITTED",
"EPS CONTRIBUTION REMITTED",
"EPF EPS DIFFERENCE REMITTED",
"NCP DAYS",
"REFUNDED OF ADVANCES"
]
rows = self._get_employee_pf_data()
for col, header in enumerate(headers):
worksheet.write(row, col, header, style_highlight)
worksheet.set_column(col, col, 30)
row = 1
for data_row in rows:
col = 0
worksheet.set_row(row, 20)
for data in data_row:
worksheet.write(row, col, data, style_normal)
col += 1
row += 1
workbook.close()
xlsx_data = output.getvalue()
self.xls_file = base64.encodebytes(xlsx_data)
self.xls_filename = f"{self.display_name} EPF Report.xlsx"
+342
View File
@@ -0,0 +1,342 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import calendar
import io
from datetime import date, datetime
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from .report_hr_epf import MONTH_SELECTION
class HrESICReport(models.Model):
_name = 'l10n.in.hr.payroll.esic.report'
_description = 'Indian Payroll: Employee State Insurance Report / Employees State Insurance Corporation'
def _get_year_selection(self):
current_year = datetime.now().year
return [(str(i), i) for i in range(current_year, 1989, -1)]
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
month = fields.Selection(MONTH_SELECTION, default=lambda self: str(datetime.today().month), required=True)
year = fields.Selection(selection='_get_year_selection', required=True, default=lambda self: str(datetime.now().year))
export_report_type = fields.Selection(
[('esi', 'ESI Summary'), ('esic', 'ESIC')],
default='esi', required=True, string="Report Type"
)
period_has_payslips = fields.Boolean(compute='_compute_period_has_payslips')
xlsx_file = fields.Binary(string="Generated File")
xlsx_filename = fields.Char()
_unique_esic_report_per_month_year = models.Constraint(
'UNIQUE(company_id, month, year, export_report_type)',
"An ESI/ESIC Report for this month and year already exists.",
)
@api.model
def default_get(self, fields):
if self.env.company.country_id.code != "IN":
raise UserError(_('You must be logged in a Indian company to use this feature'))
return super().default_get(fields)
@api.depends('month', 'year')
def _compute_display_name(self):
month_description = dict(self._fields['month']._description_selection(self.env))
for report in self:
report.display_name = f"{month_description.get(report.month)}-{report.year}"
def _get_period_payslips_with_employees(self):
self.ensure_one()
indian_employees = self.env['hr.employee'].search([
('company_id', '=', self.company_id.id),
('company_id.l10n_in_esic', '=', True),
('version_id.l10n_in_esic_employee_amount', '>', 0),
('version_id.l10n_in_esic_employer_amount', '>', 0),
])
year = int(self.year)
month = int(self.month)
end_date = calendar.monthrange(year, month)[1]
payslips = self.env['hr.payslip'].search([
('employee_id', 'in', indian_employees.ids),
('date_from', '>=', date(year, month, 1)),
('date_to', '<=', date(year, month, end_date)),
('state', 'in', ('validated', 'paid'))
])
return indian_employees, payslips
@api.depends('month', 'year')
def _compute_period_has_payslips(self):
for report in self:
_, payslips = report._get_period_payslips_with_employees()
report.period_has_payslips = bool(payslips)
def _get_employee_esic_data(self):
self.ensure_one()
# Get the relevant records based on the year and month
result = []
indian_employees, payslips = self._get_period_payslips_with_employees()
if not payslips:
return []
payslip_line_values = payslips._get_line_values(['GROSS'])
for employee in indian_employees:
gross_wage = 0
working_days = 0
payslip_ids = payslips.filtered(lambda p: p.employee_id == employee)
if not payslip_ids:
continue
for payslip in payslip_ids:
gross_wage += payslip_line_values['GROSS'][payslip.id]['total']
working_days = sum(workday.number_of_days for workday in payslip.worked_days_line_ids if workday.is_paid)
result.append((
employee.l10n_in_esic_number,
employee.name,
working_days,
gross_wage,
0,
employee.version_id.contract_date_end.strftime('%d-%m-%Y') if employee.version_id.contract_date_end else ''
))
return result
def _get_employee_esi_data(self):
self.ensure_one()
# Get the relevant records based on the year and month
result = []
indian_employees, payslips = self._get_period_payslips_with_employees()
if not payslips:
return []
payslip_line_values = payslips._get_line_values(['GROSS', 'ESICF', 'ESICS'])
for employee in indian_employees:
esi_wage_base = 0
employee_esi_contribution = 0
employer_esi_contribution = 0
payslip_ids = payslips.filtered(lambda p: p.employee_id == employee)
for payslip in payslip_ids:
esi_wage_base += payslip_line_values['GROSS'][payslip.id]['total']
employee_esi_contribution -= payslip_line_values['ESICS'][payslip.id]['total']
employer_esi_contribution += payslip_line_values['ESICF'][payslip.id]['total']
if not employee_esi_contribution and not employer_esi_contribution:
continue
result.append((
employee.name,
employee.registration_number or '-',
employee.l10n_in_esic_number or '-',
esi_wage_base,
employee_esi_contribution,
employer_esi_contribution,
employee_esi_contribution + employer_esi_contribution # Total Contribution by employee and employer
))
return result
def action_export_xlsx(self):
self.ensure_one()
output = io.BytesIO()
import xlsxwriter # noqa: PLC0415
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
if self.export_report_type == 'esi':
self.action_export_esi_xlsx(output, workbook)
else:
self.action_export_esic_xlsx(output, workbook)
def action_export_esi_xlsx(self, output, workbook):
self.ensure_one()
worksheet = workbook.add_worksheet(_('Employee_ESI_report'))
style_highlight = workbook.add_format({'bold': True, 'pattern': 1, 'bg_color': '#E0E0E0', 'align': 'center'})
style_normal = workbook.add_format({'font_size': 12})
row = 0
worksheet.set_row(row, 20)
headers = [
_("EMPLOYEE NAME"),
_("EMPLOYEE NUMBER"),
_("STATUTORY REGISTRATION NUMBER"),
_("ESI WAGES"),
_("EMPLOYEE CONTRIBUTION"),
_("EMPLOYER CONTRIBUTION"),
_("TOTAL REMITTED"),
]
rows = self._get_employee_esi_data()
if not rows:
raise ValidationError(_('No Employees on the ESI report for the selected period'))
for col, header in enumerate(headers):
worksheet.write(row, col, header, style_highlight)
worksheet.set_column(col, col, 33)
row = 1
for data_row in rows:
col = 0
worksheet.set_row(row, 20)
for data in data_row:
worksheet.write(row, col, data, style_normal)
col += 1
row += 1
workbook.close()
xlsx_data = output.getvalue()
self.xlsx_file = base64.encodebytes(xlsx_data)
self.xlsx_filename = _("%(display_name)s ESI Report.xlsx", display_name=self.display_name)
def action_export_esic_xlsx(self, output, workbook):
self.ensure_one()
esic_worksheet = workbook.add_worksheet(_('Employees\' State Insurance Corporation'))
instruction_worksheet = workbook.add_worksheet(_('Instructions & Reason Codes'))
# Header style with bold and background color, including text wrapping
style_highlight = workbook.add_format({
'bold': True,
'pattern': 1,
'bg_color': '#E0E0E0',
'align': 'center',
'valign': 'vcenter',
'text_wrap': True,
'border': 1,
'border_color': '#000000',
})
style_normal = workbook.add_format({'font_size': 12})
row = 0
esic_headers = [
_("IP Number (10 Digits)"),
_("IP Name (Only alphabets and space)"),
_("No of Days for which wages paid/payable during the month"),
_("Total Monthly Wages"),
_("Reason Code for Zero workings days(Numeric Only: provide 0 for all other reasons)"),
_("Last Working Day (Format DD/MM/YYYY or DD-MM-YYYY)"),
]
esic_rows = self._get_employee_esic_data()
# Writing the esic_headers
esic_worksheet.set_row(row, 35)
for col, header in enumerate(esic_headers):
esic_worksheet.write(row, col, header, style_highlight)
esic_worksheet.set_column(col, col, 40)
row = 1
for data_row in esic_rows:
col = 0
for data in data_row:
esic_worksheet.write(row, col, data, style_normal)
col += 1
row += 1
# create another worksheet for info & reason code
reason_code_style_normal = workbook.add_format({
'align': 'left',
'valign': 'top',
'text_wrap': True,
'font_size': 12,
})
reason_code_style_col = workbook.add_format({
'align': 'center',
'text_wrap': True,
'font_size': 12,
})
reason_code_header = [
"Reason",
"Code",
"Note",
]
row = 0
for col, header in enumerate(reason_code_header):
instruction_worksheet.write(row, col, header, style_highlight)
instruction_worksheet.set_column(0, 0, 35)
instruction_worksheet.set_column(1, 1, 20)
instruction_worksheet.set_column(2, 2, 85)
reason_codes_rows = [
[_("Without Reason"), "0", _("Leave last working day as blank")],
[_("On Leave"), "1", _("Leave last working day as blank")],
[_("Left Service"), "2", _("Please provide last working day (dd/mm/yyyy). IP will not appear from next wage period")],
[_("Retired"), "3", _("Please provide last working day (dd/mm/yyyy). IP will not appear from next wage period")],
[_("Out of Coverage"), "4", _("Please provide last working day (dd/mm/yyyy). IP will not appear from next contribution "
"period. This option is valid only if Wage Period is April/October. In case any other month then IP will continue "
"to appear in the list")],
[_("Expired"), "5", _("Please provide last working day (dd/mm/yyyy). IP will not appear from next wage period")],
[_("Non Implemented area"), "6", _("Please provide last working day (dd/mm/yyyy)")],
[_("Compliance by Immediate Employer"), "7", _("Leave last working day as blank")],
[_("Suspension of work"), "8", _("Leave last working day as blank")],
[_("Strike/Lockout"), "9", _("Leave last working day as blank")],
[_("Retrenchment"), "10", _("Please provide last working day (dd/mm/yyyy). IP will not appear from next wage period")],
[_("No Work"), "11", _("Leave last working day as blank")],
[_("Doesnt Belong To This Employer"), "12", _("Leave last working day as blank")],
[_("Duplicate IP"), "13", _("Leave last working day as blank")],
]
row = 1
for data_row in reason_codes_rows:
col = 0
if row == 5:
instruction_worksheet.set_row(row, 50) # Increase the height of row 5
for data in data_row:
instruction_worksheet.write(row, col, data, reason_code_style_col if col == 1 else reason_code_style_normal)
col += 1
row += 1
row += 1
col = 0
instruction_worksheet.set_row(row, 40)
instruction_worksheet.write(row, col, _('Instructions to fill in the excel file:'), workbook.add_format({
'bold': True,
'font_size': 17,
}))
info_row = [
_("1. Enter the IP number, IP name, No. of Days, Total Monthly Wages, Reason for 0 wages(If Wages '0') & "
"Last Working Day(only if employee has left service, Retired, Out of coverage, Expired, Non-Implemented"
"area or Retrenchment. For other reasons, last working day must be left BLANK)"),
_("2. Number of days must me a whole number. Fractions should be rounded up to next higher whole number/integer"),
_("3. Excel sheet upload will lead to successful transaction only when all the Employees' (who are currently "
"mapped in the system) details are entered perfectly in the excel sheet"),
_("4. Reasons are to be assigned numeric code and date has to be provided as mentioned in the table above"),
_("5. Once 0 wages given and last working day is mentioned as in reason codes (2,3,4,5,10) IP will be "
"removed from the employer's record. Subsequent months will not have this IP listed under the employer."
"Last working day should be mentioned only if 'Number of days wages paid/payable' is '0'"),
_("6. In case IP has worked for part of the month(i.e. atleast 1 day wage is paid/payable) and left in between "
"of the month, then last working day shouldn't be mentioned"),
_("7. Calculations - IP Contribution and Employer contribution calculation will be automatically done by the system"),
_("8. Date column format is dd/mm/yyyy or dd-mm-yyyy. Pad single digit dates with 0. Eg:- 2/5/2010 or "
"2-May-2010 is NOT acceptable. Correct format is 02/05/2010 or 02-05-2010"),
_("9. Excel file should be saved in .xls format (Excel 97-2003)"),
_("10. Note that all the column including date column should be in 'Text' format"),
_("10a. To convert all columns to text"),
_(" a. Select column A; Click Data in Menu Bar on top; Select Text to Columns ; Click Next (keep default \n "
"selection of Delimited); Click Next (keep default selection of Tab); Select TEXT; Click FINISH. \n "
"Excel 97 - 2003 as well have TEXT to COLUMN conversion facility"),
_(" b. Repeat the above step for each of the 6 columns. (Columns A - F )"),
_("10b. Another method that can be used to text conversion is - copy the column with data and paste it in NOTEPAD."
"Select the column (in excel) and convert to text. Copy the data back from notepad to excel"),
_("11. If problem continues while upload, download a fresh template by clicking 'Sample MC Excel Template'. "
"Then copy the data area from Step 8a.a - eg: copy Cell A2 to F8 (if there is data in 8 rows); "
"Paste it in cell A2 in the fresh template. Upload it "),
]
row += 1
for data_row in info_row:
instruction_worksheet.set_row(row, 38)
instruction_worksheet.merge_range(row, 0, row, 2, data_row, reason_code_style_normal)
row += 1
workbook.close()
xlsx_data = output.getvalue()
self.xlsx_file = base64.encodebytes(xlsx_data)
self.xlsx_filename = _("%(display_name)s ESIC Report.xlsx", display_name=self.display_name)
@@ -0,0 +1,146 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.tools import format_date, SQL
from odoo.exceptions import UserError
class ReportL10n_In_Hr_PayrollReport_Hryearlysalary(models.AbstractModel):
_name = 'report.l10n_in_hr_payroll.report_hryearlysalary'
_description = "Indian Yearly Salary Report"
# YTI: This mess deserves a good cleaning
def _get_periods_new(self, form):
months = []
# Get start year-month-date and end year-month-date
year = int(form['year'])
# Get name of the months from integer
month_name = []
for month in range(1, 13):
month_name.append(format_date(self.env, date(year, month, 1), date_format='MMM'))
months.append(f"{month:02d}-{year}")
return [month_name], months
def _get_employee(self, form):
return self.env['hr.employee'].browse(form.get('employee_ids', []))
def _get_employee_detail_new(self, form, employee_id, months, date_from, date_to):
structures_data = {}
payslip_lines = self._cal_monthly_amt(form, employee_id, months, date_from, date_to)
for structure_name, payslip_data in payslip_lines.items():
allow_list = []
deduct_list = []
total = 0.0
gross = False
net = False
for line in payslip_data:
code = line[0]
subline = line[1:]
if code == "GROSS":
gross = [code, subline]
elif code == "NET":
net = [code, subline]
elif subline[-1] > 0.0 and code != "NET":
total += subline[-1]
allow_list.append([code, subline])
elif subline[-1] < 0.0:
total += subline[-1]
deduct_list.append([code, subline])
if gross:
allow_list.append(gross)
if net:
deduct_list.append(net)
structures_data[structure_name] = {
'allow_list': allow_list,
'deduct_list': deduct_list,
'total': total,
}
return structures_data
def _cal_monthly_amt(self, form, emp_id, months, date_from, date_to):
result = {}
salaries = {}
lang_code = self.env.context.get('lang', 'en_US')
self.env.cr.execute(SQL(
"""
SELECT src.code, pl.name, sum(pl.total), to_char(p.date_to,'mm-yyyy') as to_date, ps.name
FROM hr_payslip_line as pl
LEFT JOIN hr_salary_rule AS sr on sr.id = pl.salary_rule_id
LEFT JOIN hr_salary_rule_category AS src on (sr.category_id = src.id)
LEFT JOIN hr_payslip as p on pl.slip_id = p.id
LEFT JOIN hr_employee as e on e.id = p.employee_id
LEFT JOIN hr_payroll_structure as ps on ps.id = p.struct_id
WHERE p.employee_id = %(employee_id)s
AND p.state = 'paid'
AND p.date_from >= %(date_from)s AND p.date_to <= %(date_to)s
GROUP BY src.parent_id, pl.sequence, pl.id, sr.category_id, pl.name, p.date_to, src.code, ps.name
ORDER BY pl.sequence, src.parent_id
""", employee_id=emp_id, date_from=date_from, date_to=date_to
))
for category_code, item_name, amount, payslip_date, structure_name in self.env.cr.fetchall():
structure_name = structure_name.get(lang_code, '')
salaries.setdefault(structure_name, {}).setdefault(category_code, {}).setdefault(item_name, {}).setdefault(payslip_date, 0.0)
salaries[structure_name][category_code][item_name][payslip_date] += amount
result = {key: self.salary_list(value, months) for key, value in salaries.items()}
return result
def salary_list(self, salaries, months):
cat_salary_all = []
for code, category_amount in salaries.items():
for category_name, amount in category_amount.items():
cat_salary = [code, category_name]
total = 0.0
for month in months:
if month != 'None':
if len(month) != 7:
month = '0' + str(month)
if amount.get(month):
cat_salary.append(amount[month])
total += amount[month]
else:
cat_salary.append(0.00)
else:
cat_salary.append('')
cat_salary.append(total)
cat_salary_all.append(cat_salary)
return cat_salary_all
@api.model
def _get_report_values(self, docids, data=None):
if not self.env.context.get('active_model') or not self.env.context.get('active_id'):
raise UserError(_("Form content is missing, this report cannot be printed."))
model = self.env.context.get('active_model')
docs = self.env[model].browse(self.env.context.get('active_id'))
employees = self._get_employee(data['form'])
month_name, months = self._get_periods_new(data['form'])
date_from = fields.Date.today() + relativedelta(day=1, month=1, year=int(data['form']['year']))
date_to = fields.Date.today() + relativedelta(day=31, month=12, year=int(data['form']['year']))
employee_data = {}
for employee in employees:
structures_data = self._get_employee_detail_new(data['form'], employee.id, months, date_from, date_to)
employee_data[employee.id] = structures_data
return {
'doc_ids': docids,
'doc_model': model,
'data': data,
'docs': docs,
'date_from': date_from,
'date_to': date_to,
'get_employee': self._get_employee,
'get_periods': lambda form: month_name,
'get_employee_detail_new': lambda emp_id: employee_data.get(emp_id, {}),
}
@@ -0,0 +1,30 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo import api, models, tools
class ReportL10n_In_Hr_PayrollReport_Payrolladvice(models.AbstractModel):
_name = 'report.l10n_in_hr_payroll.report_payrolladvice'
_description = "Indian Payroll Advice Report"
def get_month(self, input_date):
res = {'from_name': '', 'to_name': ''}
slip = self.env['hr.payslip'].search([('date_from', '<=', input_date), ('date_to', '>=', input_date)], limit=1)
if slip:
res['from_name'] = tools.format_date(self.env, slip.date_from, date_format='dd-MMMM-Y')
res['to_name'] = tools.format_date(self.env, slip.date_to, date_format='dd-MMMM-Y')
return res
@api.model
def _get_report_values(self, docids, data=None):
payment_report = self.env['hr.payroll.payment.report.wizard'].browse(docids)
return {
'doc_ids': docids,
'doc_model': 'hr.payroll.payment.report.wizard',
'data': data,
'docs': payment_report,
'time': time,
'get_month': self.get_month,
}