Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,465 @@
# -*- coding: utf-8 -*-
"""
Payroll Report Framework
========================
Base abstract model for all payroll reports, modeled after Odoo's account_reports.
Provides filter options, data fetching, and export functionality.
"""
import io
import json
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.tools import format_date, float_round
from odoo.tools.misc import formatLang
class PayrollReport(models.AbstractModel):
"""
Abstract base class for all payroll reports.
Provides common filter options, column definitions, and export methods.
"""
_name = 'payroll.report'
_description = 'Payroll Report Base'
# =========================================================================
# REPORT CONFIGURATION (Override in concrete reports)
# =========================================================================
# Filter options - set to True to enable
filter_date_range = True
filter_employee = True
filter_department = False
filter_pay_period = False
# Report metadata
report_name = 'Payroll Report'
report_code = 'payroll_report'
# Date filter presets matching QuickBooks
DATE_FILTER_OPTIONS = [
('last_pay_date', 'Last pay date'),
('this_month', 'This month'),
('this_quarter', 'This quarter'),
('this_year', 'This year'),
('last_month', 'Last month'),
('last_quarter', 'Last quarter'),
('last_year', 'Last year'),
('first_quarter', 'First quarter'),
('custom', 'Custom'),
]
# =========================================================================
# OPTIONS HANDLING
# =========================================================================
def _get_default_options(self):
"""Return default options for the report."""
today = date.today()
options = {
'report_name': self.report_name,
'report_code': self.report_code,
'date': {
'filter': 'this_year',
'date_from': today.replace(month=1, day=1).strftime('%Y-%m-%d'),
'date_to': today.strftime('%Y-%m-%d'),
},
'columns': self._get_columns(),
'currency_id': self.env.company.currency_id.id,
}
if self.filter_employee:
options['employee_ids'] = []
options['all_employees'] = True
if self.filter_department:
options['department_ids'] = []
options['all_departments'] = True
return options
def _get_options(self, previous_options=None):
"""
Build report options, merging with previous options if provided.
"""
options = self._get_default_options()
if previous_options:
# Merge date options
if 'date' in previous_options:
options['date'].update(previous_options['date'])
# Merge employee filter
if 'employee_ids' in previous_options:
options['employee_ids'] = previous_options['employee_ids']
options['all_employees'] = not previous_options['employee_ids']
# Merge department filter
if 'department_ids' in previous_options:
options['department_ids'] = previous_options['department_ids']
options['all_departments'] = not previous_options['department_ids']
# Apply date filter preset
self._apply_date_filter(options)
return options
def _apply_date_filter(self, options):
"""Calculate date_from and date_to based on filter preset."""
today = date.today()
date_filter = options.get('date', {}).get('filter', 'this_year')
if date_filter == 'custom':
# Keep existing dates
return
date_from = today
date_to = today
if date_filter == 'last_pay_date':
# Find the last payslip date
last_payslip = self.env['hr.payslip'].search([
('state', 'in', ['done', 'paid']),
('company_id', '=', self.env.company.id),
], order='date_to desc', limit=1)
if last_payslip:
date_from = last_payslip.date_from
date_to = last_payslip.date_to
else:
date_from = today
date_to = today
elif date_filter == 'this_month':
date_from = today.replace(day=1)
date_to = (today.replace(day=1) + relativedelta(months=1)) - relativedelta(days=1)
elif date_filter == 'this_quarter':
quarter_month = ((today.month - 1) // 3) * 3 + 1
date_from = today.replace(month=quarter_month, day=1)
date_to = (date_from + relativedelta(months=3)) - relativedelta(days=1)
elif date_filter == 'this_year':
date_from = today.replace(month=1, day=1)
date_to = today.replace(month=12, day=31)
elif date_filter == 'last_month':
last_month = today - relativedelta(months=1)
date_from = last_month.replace(day=1)
date_to = today.replace(day=1) - relativedelta(days=1)
elif date_filter == 'last_quarter':
quarter_month = ((today.month - 1) // 3) * 3 + 1
last_quarter_start = today.replace(month=quarter_month, day=1) - relativedelta(months=3)
date_from = last_quarter_start
date_to = (last_quarter_start + relativedelta(months=3)) - relativedelta(days=1)
elif date_filter == 'last_year':
date_from = today.replace(year=today.year - 1, month=1, day=1)
date_to = today.replace(year=today.year - 1, month=12, day=31)
elif date_filter == 'first_quarter':
date_from = today.replace(month=1, day=1)
date_to = today.replace(month=3, day=31)
options['date']['date_from'] = date_from.strftime('%Y-%m-%d')
options['date']['date_to'] = date_to.strftime('%Y-%m-%d')
# =========================================================================
# COLUMN DEFINITIONS (Override in concrete reports)
# =========================================================================
def _get_columns(self):
"""
Return column definitions for the report.
Override in concrete reports.
Returns:
list: List of column dictionaries with:
- name: Display name
- field: Data field name
- type: 'char', 'date', 'monetary', 'float', 'integer'
- sortable: bool
- width: optional width class
"""
return [
{'name': _('Name'), 'field': 'name', 'type': 'char', 'sortable': True},
]
# =========================================================================
# DATA FETCHING (Override in concrete reports)
# =========================================================================
def _get_domain(self, options):
"""
Build search domain from options.
Override in concrete reports for specific filtering.
"""
domain = [('company_id', '=', self.env.company.id)]
# Date filter
date_from = options.get('date', {}).get('date_from')
date_to = options.get('date', {}).get('date_to')
if date_from:
domain.append(('date_from', '>=', date_from))
if date_to:
domain.append(('date_to', '<=', date_to))
# Employee filter
if not options.get('all_employees') and options.get('employee_ids'):
domain.append(('employee_id', 'in', options['employee_ids']))
# Department filter
if not options.get('all_departments') and options.get('department_ids'):
domain.append(('employee_id.department_id', 'in', options['department_ids']))
return domain
def _get_lines(self, options):
"""
Fetch and return report lines.
Override in concrete reports.
Returns:
list: List of line dictionaries with:
- id: unique line id
- name: display name
- columns: list of column values
- level: hierarchy level (0, 1, 2...)
- unfoldable: bool for drill-down
- unfolded: bool current state
- class: CSS classes
"""
return []
def _get_report_data(self, options):
"""
Get complete report data for frontend.
"""
return {
'options': options,
'columns': self._get_columns(),
'lines': self._get_lines(options),
'date_filter_options': self.DATE_FILTER_OPTIONS,
}
# =========================================================================
# TOTALS AND AGGREGATIONS
# =========================================================================
def _get_total_line(self, lines, options):
"""
Calculate totals from lines.
Override for custom total calculations.
"""
if not lines:
return {}
columns = self._get_columns()
total_values = {}
for col in columns:
if col.get('type') in ('monetary', 'float', 'integer'):
field = col['field']
try:
total_values[field] = sum(
float(line.get('values', {}).get(field, 0) or 0)
for line in lines
if line.get('level', 0) == 0 # Only sum top-level lines
)
except (ValueError, TypeError):
total_values[field] = 0
return {
'id': 'total',
'name': _('Total'),
'values': total_values,
'level': -1, # Special level for total
'class': 'o_payroll_report_total fw-bold',
}
# =========================================================================
# SETTINGS HELPERS
# =========================================================================
def _get_payroll_settings(self):
"""Get payroll settings for current company."""
return self.env['payroll.config.settings'].get_settings()
def _get_company_legal_info(self):
"""Get company legal name and address from settings."""
settings = self._get_payroll_settings()
return {
'legal_name': settings.company_legal_name or self.env.company.name,
'legal_street': settings.company_legal_street or self.env.company.street or '',
'legal_street2': settings.company_legal_street2 or self.env.company.street2 or '',
'legal_city': settings.company_legal_city or self.env.company.city or '',
'legal_state': settings.company_legal_state_id.name if settings.company_legal_state_id else (self.env.company.state_id.name if self.env.company.state_id else ''),
'legal_zip': settings.company_legal_zip or self.env.company.zip or '',
'legal_country': settings.company_legal_country_id.name if settings.company_legal_country_id else (self.env.company.country_id.name if self.env.company.country_id else ''),
}
def _get_payroll_contact_info(self):
"""Get payroll contact information from settings."""
settings = self._get_payroll_settings()
return {
'name': settings.get_payroll_contact_name(),
'phone': settings.payroll_contact_phone or '',
'email': settings.payroll_contact_email or '',
}
# =========================================================================
# FORMATTING HELPERS
# =========================================================================
def _format_value(self, value, column_type, options):
"""Format value based on column type."""
if value is None:
return ''
currency = self.env['res.currency'].browse(options.get('currency_id'))
if column_type == 'monetary':
return formatLang(self.env, value, currency_obj=currency)
elif column_type == 'float':
return formatLang(self.env, value, digits=2)
elif column_type == 'integer':
return str(int(value))
elif column_type == 'date':
if isinstance(value, str):
value = fields.Date.from_string(value)
return format_date(self.env, value)
elif column_type == 'percentage':
return f"{float_round(value * 100, 2)}%"
else:
return str(value) if value else ''
# =========================================================================
# EXPORT METHODS
# =========================================================================
def get_xlsx(self, options):
"""
Generate Excel export of the report.
Returns binary data.
"""
try:
import xlsxwriter
except ImportError:
raise ImportError("xlsxwriter library is required for Excel export")
output = io.BytesIO()
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
sheet = workbook.add_worksheet(self.report_name[:31]) # Max 31 chars
# Styles
header_style = workbook.add_format({
'bold': True,
'bg_color': '#4a86e8',
'font_color': 'white',
'border': 1,
})
money_style = workbook.add_format({'num_format': '$#,##0.00'})
date_style = workbook.add_format({'num_format': 'yyyy-mm-dd'})
total_style = workbook.add_format({'bold': True, 'top': 2})
# Get data
columns = self._get_columns()
lines = self._get_lines(options)
# Add company info from settings
company_info = self._get_company_legal_info()
contact_info = self._get_payroll_contact_info()
# Write report header with company info
sheet.write(0, 0, company_info['legal_name'] or self.env.company.name, header_style)
if company_info['legal_street']:
sheet.write(1, 0, company_info['legal_street'])
if company_info['legal_city']:
addr_line = f"{company_info['legal_city']}, {company_info['legal_state']} {company_info['legal_zip']}"
sheet.write(2, 0, addr_line)
if contact_info['name']:
sheet.write(3, 0, f"Contact: {contact_info['name']}")
# Write headers (starting at row 5)
header_row = 5
for col_idx, col in enumerate(columns):
sheet.write(header_row, col_idx, col['name'], header_style)
sheet.set_column(col_idx, col_idx, 15)
# Write data (starting after header row)
row = header_row + 1
for line in lines:
values = line.get('values', {})
style = total_style if line.get('level', 0) < 0 else None
for col_idx, col in enumerate(columns):
value = values.get(col['field'], '')
cell_style = style
if col['type'] == 'monetary':
cell_style = money_style
elif col['type'] == 'date':
cell_style = date_style
if cell_style:
sheet.write(row, col_idx, value, cell_style)
else:
sheet.write(row, col_idx, value)
row += 1
workbook.close()
output.seek(0)
return output.read()
def get_pdf(self, options):
"""
Generate PDF export of the report.
Returns binary data.
"""
report_data = self._get_report_data(options)
# Get settings data
company_info = self._get_company_legal_info()
contact_info = self._get_payroll_contact_info()
# Render QWeb template
html = self.env['ir.qweb']._render(
'fusion_payroll.payroll_report_pdf_template',
{
'report': self,
'options': options,
'columns': report_data['columns'],
'lines': report_data['lines'],
'company': self.env.company,
'company_info': company_info,
'contact_info': contact_info,
'format_value': self._format_value,
}
)
# Convert to PDF using wkhtmltopdf
pdf = self.env['ir.actions.report']._run_wkhtmltopdf(
[html],
landscape=True,
specific_paperformat_args={'data-report-margin-top': 10}
)
return pdf
# =========================================================================
# ACTION METHODS
# =========================================================================
def action_open_report(self):
"""Open the report in client action."""
return {
'type': 'ir.actions.client',
'tag': 'fusion_payroll.payroll_report_action',
'name': self.report_name,
'context': {
'report_model': self._name,
},
}