Theme Files Added

This commit is contained in:
vidhubk 2026-03-05 17:08:54 +05:30
commit 78d5dc53ea
43 changed files with 1354 additions and 0 deletions

View File

@ -0,0 +1 @@
# from . import models

View File

@ -0,0 +1,27 @@
{
'name': 'Accounting Community',
'version': '17.0.1.0.0',
'summary': 'Full Accounting Features for Community Edition',
'description': """
Accounting Community
====================
This module unlocks the Full Accounting features in Odoo Community Edition.
Features:
- Full Accounting Menu
- Journal Entries
- Journal Items
- General Ledger (Basic View)
- Partner Ledger (Basic View)
""",
'category': 'Accounting/Accounting',
'author': 'Antigravity',
'depends': ['account'],
'data': [
'views/account_menus.xml',
'views/account_move_views.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Rename "Invoicing" to "Accounting" -->
<record id="account.menu_finance" model="ir.ui.menu">
<field name="name">Accounting</field>
<field name="groups_id" eval="[(6, 0, [])]"/> <!-- Ensure it is visible -->
</record>
<!-- Top-level "Accounting" menu (Controller) - usually hidden in Community -->
<menuitem id="account.menu_finance_entries"
name="Accounting"
parent="account.menu_finance"
sequence="4"
groups="account.group_account_user,account.group_account_manager"/>
<!-- Journal Entries -->
<menuitem id="menu_action_move_journal_line_form"
action="account.action_move_journal_line"
parent="account.menu_finance_entries"
sequence="1"/>
<!-- Journal Items -->
<menuitem id="menu_action_account_moves_all_a"
action="account.action_account_moves_all_a"
parent="account.menu_finance_entries"
sequence="10"/>
<!-- Reporting: General Ledger -->
<!-- Note: Standard reports in Community are limited. We enable what we can or point to list views -->
<menuitem id="menu_action_account_moves_all_a_gl"
name="General Ledger (Items)"
action="account.action_account_moves_all_a"
parent="account.menu_finance_reports"
sequence="100"/>
</odoo>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Ensure the actions are available -->
<record id="account.action_move_journal_line" model="ir.actions.act_window">
<field name="name">Journal Entries</field>
<field name="res_model">account.move</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="account.view_move_tree"/>
<field name="search_view_id" ref="account.view_account_move_filter"/>
<field name="context">{'default_move_type': 'entry', 'search_default_misc_filter':1, 'view_no_maturity': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a journal entry
</p><p>
A journal entry consists of several journal items, each of
which is either a debit or a credit.
</p>
</field>
</record>
</odoo>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Placeholder for generic views if needed -->
</odoo>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
{
'name': 'C2C Payroll',
'version': '17.0.1.0.0',
'category': 'Human Resources/Payroll',
'summary': 'Enterprise-like Payroll for Odoo Community',
'description': """
Complete payroll solution for Odoo 17 Community Edition.
Features:
- Salary Structures (Basic, HRA, Allowances)
- Automatic PF, ESI, Professional Tax deductions
- Payslip generation with auto-computed net salary
- Accounting journal entry on payslip confirmation
- QWeb PDF payslip report
- Multi-company support
""",
'author': 'C2C',
'website': '',
'license': 'LGPL-3',
'depends': [
'hr',
'hr_contract',
'account',
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'views/salary_structure_views.xml',
'views/payslip_views.xml',
'views/contract_views.xml',
'views/menu.xml',
'reports/payslip_report.xml',
'reports/payslip_report_template.xml',
],
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import salary_structure
from . import contract_extension
from . import payslip

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ContractExtension(models.Model):
_inherit = 'hr.contract'
salary_structure_id = fields.Many2one(
'c2c.salary.structure', string='Salary Structure',
help='Salary structure used for payslip computation.',
)
gross_salary = fields.Float(
string='Gross Salary',
help='Total gross salary (CTC) before deductions.',
)
pf_applicable = fields.Boolean(
string='PF Applicable', default=True,
help='Whether Provident Fund deduction applies to this contract.',
)
esi_applicable = fields.Boolean(
string='ESI Applicable', default=False,
help='Whether ESI deduction applies to this contract.',
)

View File

@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
class Payslip(models.Model):
_name = 'c2c.payslip'
_description = 'Employee Payslip'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_from desc, id desc'
_rec_name = 'display_name'
# ------------------------------------------------------------------
# Core fields
# ------------------------------------------------------------------
employee_id = fields.Many2one(
'hr.employee', string='Employee', required=True,
tracking=True,
)
contract_id = fields.Many2one(
'hr.contract', string='Contract', required=True,
tracking=True,
domain="[('employee_id', '=', employee_id), ('state', '=', 'open')]",
)
date_from = fields.Date(
string='Period From', required=True,
)
date_to = fields.Date(
string='Period To', required=True,
)
company_id = fields.Many2one(
'res.company', string='Company', required=True,
default=lambda self: self.env.company,
)
# ------------------------------------------------------------------
# Earnings (computed & stored)
# ------------------------------------------------------------------
gross_salary = fields.Float(
string='Gross Salary', compute='_compute_salary', store=True,
)
basic = fields.Float(
string='Basic', compute='_compute_salary', store=True,
)
hra = fields.Float(
string='HRA', compute='_compute_salary', store=True,
)
allowances = fields.Float(
string='Allowances', compute='_compute_salary', store=True,
)
# ------------------------------------------------------------------
# Deductions (computed & stored)
# ------------------------------------------------------------------
pf_deduction = fields.Float(
string='PF Deduction', compute='_compute_salary', store=True,
)
esi_deduction = fields.Float(
string='ESI Deduction', compute='_compute_salary', store=True,
)
professional_tax = fields.Float(
string='Professional Tax', compute='_compute_salary', store=True,
)
total_deductions = fields.Float(
string='Total Deductions', compute='_compute_salary', store=True,
)
# ------------------------------------------------------------------
# Net
# ------------------------------------------------------------------
net_salary = fields.Float(
string='Net Salary', compute='_compute_salary', store=True,
)
# ------------------------------------------------------------------
# State & Accounting
# ------------------------------------------------------------------
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('paid', 'Paid'),
], string='Status', default='draft', tracking=True, copy=False)
journal_entry_id = fields.Many2one(
'account.move', string='Journal Entry', readonly=True, copy=False,
)
journal_entry_count = fields.Integer(
compute='_compute_journal_entry_count',
)
# ------------------------------------------------------------------
# Display name
# ------------------------------------------------------------------
display_name = fields.Char(compute='_compute_display_name', store=True)
@api.depends('employee_id', 'date_from', 'date_to')
def _compute_display_name(self):
for rec in self:
emp_name = rec.employee_id.name or 'New'
date_from = rec.date_from or ''
date_to = rec.date_to or ''
rec.display_name = '%s (%s - %s)' % (emp_name, date_from, date_to)
# ------------------------------------------------------------------
# Computed salary
# ------------------------------------------------------------------
@api.depends(
'contract_id',
'contract_id.gross_salary',
'contract_id.salary_structure_id',
'contract_id.salary_structure_id.basic_percentage',
'contract_id.salary_structure_id.hra_percentage',
'contract_id.salary_structure_id.allowance_percentage',
'contract_id.salary_structure_id.pf_percentage',
'contract_id.salary_structure_id.esi_percentage',
'contract_id.salary_structure_id.professional_tax_fixed',
'contract_id.pf_applicable',
'contract_id.esi_applicable',
)
def _compute_salary(self):
for rec in self:
contract = rec.contract_id
structure = contract.salary_structure_id if contract else False
if not contract or not structure:
rec.gross_salary = 0.0
rec.basic = 0.0
rec.hra = 0.0
rec.allowances = 0.0
rec.pf_deduction = 0.0
rec.esi_deduction = 0.0
rec.professional_tax = 0.0
rec.total_deductions = 0.0
rec.net_salary = 0.0
continue
gross = contract.gross_salary
rec.gross_salary = gross
# Earnings
rec.basic = gross * structure.basic_percentage / 100.0
rec.hra = gross * structure.hra_percentage / 100.0
rec.allowances = gross * structure.allowance_percentage / 100.0
# Deductions
pf = (rec.basic * structure.pf_percentage / 100.0) if contract.pf_applicable else 0.0
esi = (gross * structure.esi_percentage / 100.0) if contract.esi_applicable else 0.0
pt = structure.professional_tax_fixed
rec.pf_deduction = pf
rec.esi_deduction = esi
rec.professional_tax = pt
rec.total_deductions = pf + esi + pt
rec.net_salary = gross - rec.total_deductions
# ------------------------------------------------------------------
# Journal entry count
# ------------------------------------------------------------------
@api.depends('journal_entry_id')
def _compute_journal_entry_count(self):
for rec in self:
rec.journal_entry_count = 1 if rec.journal_entry_id else 0
# ------------------------------------------------------------------
# Onchange helpers
# ------------------------------------------------------------------
@api.onchange('employee_id')
def _onchange_employee_id(self):
"""Auto-fill contract when employee changes."""
if self.employee_id:
contract = self.env['hr.contract'].search([
('employee_id', '=', self.employee_id.id),
('state', '=', 'open'),
('company_id', '=', self.env.company.id),
], limit=1)
self.contract_id = contract
else:
self.contract_id = False
# ------------------------------------------------------------------
# Constraints
# ------------------------------------------------------------------
@api.constrains('date_from', 'date_to')
def _check_dates(self):
for rec in self:
if rec.date_from and rec.date_to and rec.date_from > rec.date_to:
raise ValidationError('Period From cannot be after Period To.')
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_confirm(self):
"""Confirm the payslip and create an accounting journal entry."""
for rec in self:
if rec.state != 'draft':
raise UserError('Only draft payslips can be confirmed.')
if not rec.contract_id or not rec.contract_id.salary_structure_id:
raise UserError(
'Please set a salary structure on the contract before confirming.'
)
if rec.net_salary <= 0:
raise UserError('Net salary must be greater than zero to confirm.')
# Get accounts from system parameters (company-specific)
ICP = self.env['ir.config_parameter'].sudo()
expense_account_id = int(
ICP.get_param('c2c_payroll.salary_expense_account_id', default=0)
)
payable_account_id = int(
ICP.get_param('c2c_payroll.salary_payable_account_id', default=0)
)
if not expense_account_id or not payable_account_id:
raise UserError(
'Please configure Salary Expense and Payable accounts in '
'Settings → Technical → Parameters → System Parameters.\n\n'
'Keys:\n'
' • c2c_payroll.salary_expense_account_id\n'
' • c2c_payroll.salary_payable_account_id'
)
# Validate accounts exist
expense_account = self.env['account.account'].browse(expense_account_id)
payable_account = self.env['account.account'].browse(payable_account_id)
if not expense_account.exists() or not payable_account.exists():
raise UserError(
'Configured salary accounts do not exist. '
'Please verify the system parameter values.'
)
journal = self.env['account.journal'].search([
('type', '=', 'general'),
('company_id', '=', rec.company_id.id),
], limit=1)
if not journal:
raise UserError(
'No general journal found for company %s.' % rec.company_id.name
)
move_vals = {
'journal_id': journal.id,
'date': rec.date_to,
'ref': 'Payslip: %s' % rec.display_name,
'company_id': rec.company_id.id,
'line_ids': [
(0, 0, {
'name': 'Salary Expense - %s' % rec.employee_id.name,
'account_id': expense_account_id,
'debit': rec.net_salary,
'credit': 0.0,
}),
(0, 0, {
'name': 'Employee Payable - %s' % rec.employee_id.name,
'account_id': payable_account_id,
'debit': 0.0,
'credit': rec.net_salary,
}),
],
}
move = self.env['account.move'].create(move_vals)
rec.write({
'state': 'confirmed',
'journal_entry_id': move.id,
})
def action_mark_paid(self):
"""Mark confirming payslip as paid."""
for rec in self:
if rec.state != 'confirmed':
raise UserError('Only confirmed payslips can be marked as paid.')
rec.write({'state': 'paid'})
def action_reset_to_draft(self):
"""Reset to draft (deletes journal entry if unposted)."""
for rec in self:
if rec.state == 'paid':
raise UserError('Paid payslips cannot be reset to draft.')
if rec.journal_entry_id:
if rec.journal_entry_id.state == 'posted':
raise UserError(
'Cannot reset: the journal entry is already posted. '
'Please cancel it first.'
)
rec.journal_entry_id.unlink()
rec.write({'state': 'draft', 'journal_entry_id': False})
def action_open_journal_entry(self):
"""Smart button to open the linked journal entry."""
self.ensure_one()
if not self.journal_entry_id:
raise UserError('No journal entry linked to this payslip.')
return {
'type': 'ir.actions.act_window',
'name': 'Journal Entry',
'res_model': 'account.move',
'res_id': self.journal_entry_id.id,
'view_mode': 'form',
'target': 'current',
}

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class SalaryStructure(models.Model):
_name = 'c2c.salary.structure'
_description = 'Salary Structure'
_rec_name = 'name'
name = fields.Char(string='Name', required=True)
basic_percentage = fields.Float(
string='Basic (%)', required=True, default=50.0,
help='Percentage of gross salary allocated as Basic.',
)
hra_percentage = fields.Float(
string='HRA (%)', required=True, default=20.0,
help='Percentage of gross salary allocated as HRA.',
)
allowance_percentage = fields.Float(
string='Allowances (%)', required=True, default=30.0,
help='Percentage of gross salary allocated as Allowances.',
)
pf_percentage = fields.Float(
string='PF (%)', required=True, default=12.0,
help='Provident Fund deduction percentage on Basic salary.',
)
esi_percentage = fields.Float(
string='ESI (%)', required=True, default=1.75,
help='ESI deduction percentage on Gross salary.',
)
professional_tax_fixed = fields.Float(
string='Professional Tax (Fixed)', default=200.0,
help='Fixed professional tax amount deducted per month.',
)
company_id = fields.Many2one(
'res.company', string='Company', required=True,
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
@api.constrains('basic_percentage', 'hra_percentage', 'allowance_percentage')
def _check_percentages(self):
for rec in self:
total = rec.basic_percentage + rec.hra_percentage + rec.allowance_percentage
if abs(total - 100.0) > 0.01:
raise models.ValidationError(
'Basic + HRA + Allowances percentages must equal 100%%. '
'Current total: %.2f%%' % total
)

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Report Action: Payslip PDF -->
<!-- ============================================================ -->
<record id="action_report_payslip" model="ir.actions.report">
<field name="name">Payslip</field>
<field name="model">c2c.payslip</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">c2c_payroll.report_payslip_document</field>
<field name="report_file">c2c_payroll.report_payslip_document</field>
<field name="binding_model_id" ref="model_c2c_payslip"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_payslip_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page" style="font-family: Arial, sans-serif;">
<!-- ============================================ -->
<!-- Header -->
<!-- ============================================ -->
<div class="row mb-3">
<div class="col-12 text-center">
<h2 style="color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px;">
<strong>PAYSLIP</strong>
</h2>
</div>
</div>
<!-- ============================================ -->
<!-- Employee &amp; Period Details -->
<!-- ============================================ -->
<div class="row mb-4" style="background-color: #f8f9fa; padding: 15px; border-radius: 5px;">
<div class="col-6">
<table class="table table-borderless table-sm mb-0">
<tr>
<td><strong>Employee:</strong></td>
<td><span t-field="o.employee_id.name"/></td>
</tr>
<tr>
<td><strong>Department:</strong></td>
<td><span t-field="o.employee_id.department_id.name"/></td>
</tr>
<tr>
<td><strong>Job Position:</strong></td>
<td><span t-field="o.employee_id.job_id.name"/></td>
</tr>
</table>
</div>
<div class="col-6">
<table class="table table-borderless table-sm mb-0">
<tr>
<td><strong>Period:</strong></td>
<td>
<span t-field="o.date_from"/>
<span t-field="o.date_to"/>
</td>
</tr>
<tr>
<td><strong>Company:</strong></td>
<td><span t-field="o.company_id.name"/></td>
</tr>
<tr>
<td><strong>Status:</strong></td>
<td><span t-field="o.state"/></td>
</tr>
</table>
</div>
</div>
<!-- ============================================ -->
<!-- Earnings Table -->
<!-- ============================================ -->
<h4 style="color: #27ae60; margin-top: 20px;">
<i class="fa fa-plus-circle"/> Earnings
</h4>
<table class="table table-bordered table-sm">
<thead style="background-color: #27ae60; color: white;">
<tr>
<th>Component</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>Basic Salary</td>
<td class="text-end">
<span t-field="o.basic"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr>
<td>House Rent Allowance (HRA)</td>
<td class="text-end">
<span t-field="o.hra"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr>
<td>Other Allowances</td>
<td class="text-end">
<span t-field="o.allowances"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr style="font-weight: bold; background-color: #eafaf1;">
<td>Gross Salary</td>
<td class="text-end">
<span t-field="o.gross_salary"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<!-- ============================================ -->
<!-- Deductions Table -->
<!-- ============================================ -->
<h4 style="color: #e74c3c; margin-top: 20px;">
<i class="fa fa-minus-circle"/> Deductions
</h4>
<table class="table table-bordered table-sm">
<thead style="background-color: #e74c3c; color: white;">
<tr>
<th>Component</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr t-if="o.pf_deduction > 0">
<td>Provident Fund (PF)</td>
<td class="text-end">
<span t-field="o.pf_deduction"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr t-if="o.esi_deduction > 0">
<td>Employee State Insurance (ESI)</td>
<td class="text-end">
<span t-field="o.esi_deduction"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr t-if="o.professional_tax > 0">
<td>Professional Tax</td>
<td class="text-end">
<span t-field="o.professional_tax"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
<tr style="font-weight: bold; background-color: #fdedec;">
<td>Total Deductions</td>
<td class="text-end">
<span t-field="o.total_deductions"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<!-- ============================================ -->
<!-- Net Salary — Highlighted -->
<!-- ============================================ -->
<div class="row mt-4">
<div class="col-12">
<table class="table table-bordered">
<tr style="background-color: #2c3e50; color: white; font-size: 18px;">
<td class="py-3"><strong>NET SALARY PAYABLE</strong></td>
<td class="text-end py-3">
<strong>
<span t-field="o.net_salary"
t-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</strong>
</td>
</tr>
</table>
</div>
</div>
<!-- ============================================ -->
<!-- Footer note -->
<!-- ============================================ -->
<div class="row mt-4">
<div class="col-12 text-center text-muted" style="font-size: 11px;">
<p>This is a computer-generated payslip and does not require a signature.</p>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_salary_structure_user,c2c.salary.structure.user,model_c2c_salary_structure,c2c_payroll.group_payroll_user,1,0,0,0
access_salary_structure_manager,c2c.salary.structure.manager,model_c2c_salary_structure,c2c_payroll.group_payroll_manager,1,1,1,1
access_payslip_user,c2c.payslip.user,model_c2c_payslip,c2c_payroll.group_payroll_user,1,1,1,0
access_payslip_manager,c2c.payslip.manager,model_c2c_payslip,c2c_payroll.group_payroll_manager,1,1,1,1
access_payslip_wizard_user,c2c.payslip.generate.wizard.user,model_c2c_payslip_generate_wizard,c2c_payroll.group_payroll_user,1,1,1,1
access_payslip_wizard_manager,c2c.payslip.generate.wizard.manager,model_c2c_payslip_generate_wizard,c2c_payroll.group_payroll_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_salary_structure_user c2c.salary.structure.user model_c2c_salary_structure c2c_payroll.group_payroll_user 1 0 0 0
3 access_salary_structure_manager c2c.salary.structure.manager model_c2c_salary_structure c2c_payroll.group_payroll_manager 1 1 1 1
4 access_payslip_user c2c.payslip.user model_c2c_payslip c2c_payroll.group_payroll_user 1 1 1 0
5 access_payslip_manager c2c.payslip.manager model_c2c_payslip c2c_payroll.group_payroll_manager 1 1 1 1
6 access_payslip_wizard_user c2c.payslip.generate.wizard.user model_c2c_payslip_generate_wizard c2c_payroll.group_payroll_user 1 1 1 1
7 access_payslip_wizard_manager c2c.payslip.generate.wizard.manager model_c2c_payslip_generate_wizard c2c_payroll.group_payroll_manager 1 1 1 1

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Module Category -->
<!-- ============================================================ -->
<record id="module_category_c2c_payroll" model="ir.module.category">
<field name="name">C2C Payroll</field>
<field name="description">Access rights for C2C Payroll module.</field>
<field name="sequence">30</field>
</record>
<!-- ============================================================ -->
<!-- Security Groups -->
<!-- ============================================================ -->
<record id="group_payroll_user" model="res.groups">
<field name="name">Payroll User</field>
<field name="category_id" ref="module_category_c2c_payroll"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_payroll_manager" model="res.groups">
<field name="name">Payroll Manager</field>
<field name="category_id" ref="module_category_c2c_payroll"/>
<field name="implied_ids" eval="[(4, ref('c2c_payroll.group_payroll_user'))]"/>
</record>
<!-- ============================================================ -->
<!-- Record Rules — multi-company -->
<!-- ============================================================ -->
<record id="rule_payslip_company" model="ir.rule">
<field name="name">Payslip: multi-company</field>
<field name="model_id" ref="model_c2c_payslip"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="rule_salary_structure_company" model="ir.rule">
<field name="name">Salary Structure: multi-company</field>
<field name="model_id" ref="model_c2c_salary_structure"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Extend hr.contract Form — add Payroll tab -->
<!-- ============================================================ -->
<record id="view_hr_contract_form_inherit_payroll" model="ir.ui.view">
<field name="name">hr.contract.form.inherit.c2c.payroll</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Payroll" name="payroll_tab">
<group>
<group string="Salary">
<field name="salary_structure_id"/>
<field name="gross_salary"/>
</group>
<group string="Deduction Applicability">
<field name="pf_applicable"/>
<field name="esi_applicable"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Top-level Menu: Payroll under HR -->
<!-- ============================================================ -->
<menuitem id="menu_payroll_root"
name="Payroll"
parent="hr.menu_hr_root"
sequence="90"
groups="c2c_payroll.group_payroll_user"/>
<!-- ============================================================ -->
<!-- Sub-menus -->
<!-- ============================================================ -->
<menuitem id="menu_salary_structure"
name="Salary Structures"
parent="menu_payroll_root"
action="action_salary_structure"
sequence="10"
groups="c2c_payroll.group_payroll_user"/>
<menuitem id="menu_payslip"
name="Payslips"
parent="menu_payroll_root"
action="action_payslip"
sequence="20"
groups="c2c_payroll.group_payroll_user"/>
<menuitem id="menu_payslip_generate"
name="Generate Payslips"
parent="menu_payroll_root"
action="action_payslip_generate_wizard"
sequence="30"
groups="c2c_payroll.group_payroll_manager"/>
</odoo>

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Payslip — Tree View -->
<!-- ============================================================ -->
<record id="view_payslip_tree" model="ir.ui.view">
<field name="name">c2c.payslip.tree</field>
<field name="model">c2c.payslip</field>
<field name="arch" type="xml">
<tree string="Payslips" decoration-info="state == 'draft'"
decoration-success="state == 'confirmed'"
decoration-muted="state == 'paid'">
<field name="employee_id"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="gross_salary"/>
<field name="total_deductions"/>
<field name="net_salary"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-success="state == 'confirmed'"
decoration-warning="state == 'paid'"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- ============================================================ -->
<!-- Payslip — Form View -->
<!-- ============================================================ -->
<record id="view_payslip_form" model="ir.ui.view">
<field name="name">c2c.payslip.form</field>
<field name="model">c2c.payslip</field>
<field name="arch" type="xml">
<form string="Payslip">
<header>
<button name="action_confirm" string="Confirm"
type="object" class="oe_highlight"
invisible="state != 'draft'"
confirm="Are you sure you want to confirm this payslip and create a journal entry?"/>
<button name="action_mark_paid" string="Mark as Paid"
type="object" class="oe_highlight"
invisible="state != 'confirmed'"/>
<button name="action_reset_to_draft" string="Reset to Draft"
type="object"
invisible="state != 'confirmed'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirmed,paid"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_journal_entry"
type="object"
class="oe_stat_button"
icon="fa-book"
invisible="journal_entry_count == 0">
<field name="journal_entry_count" widget="statinfo"
string="Journal Entry"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="display_name" readonly="1"/>
</h1>
</div>
<group>
<group string="Employee Details">
<field name="employee_id"/>
<field name="contract_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group string="Period">
<field name="date_from"/>
<field name="date_to"/>
</group>
</group>
<notebook>
<page string="Earnings" name="earnings">
<group>
<group>
<field name="gross_salary"/>
<field name="basic"/>
<field name="hra"/>
<field name="allowances"/>
</group>
</group>
</page>
<page string="Deductions" name="deductions">
<group>
<group>
<field name="pf_deduction"/>
<field name="esi_deduction"/>
<field name="professional_tax"/>
<separator/>
<field name="total_deductions"/>
</group>
</group>
</page>
<page string="Summary" name="summary">
<group>
<group>
<field name="gross_salary" string="Gross Salary"/>
<field name="total_deductions" string="Total Deductions"/>
<separator/>
<field name="net_salary" class="oe_subtotal_footer_separator"/>
</group>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- ============================================================ -->
<!-- Payslip — Search View -->
<!-- ============================================================ -->
<record id="view_payslip_search" model="ir.ui.view">
<field name="name">c2c.payslip.search</field>
<field name="model">c2c.payslip</field>
<field name="arch" type="xml">
<search string="Payslips">
<field name="employee_id"/>
<field name="state"/>
<filter name="filter_draft" string="Draft"
domain="[('state', '=', 'draft')]"/>
<filter name="filter_confirmed" string="Confirmed"
domain="[('state', '=', 'confirmed')]"/>
<filter name="filter_paid" string="Paid"
domain="[('state', '=', 'paid')]"/>
<separator/>
<filter name="group_employee" string="Employee"
context="{'group_by': 'employee_id'}"/>
<filter name="group_state" string="Status"
context="{'group_by': 'state'}"/>
</search>
</field>
</record>
<!-- ============================================================ -->
<!-- Payslip — Action -->
<!-- ============================================================ -->
<record id="action_payslip" model="ir.actions.act_window">
<field name="name">Payslips</field>
<field name="res_model">c2c.payslip</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No payslips yet
</p>
<p>
Create payslips individually or use the "Generate Payslips" wizard
to create them in bulk for all employees with active contracts.
</p>
</field>
</record>
<!-- ============================================================ -->
<!-- Generate Payslip Wizard — Form + Action -->
<!-- ============================================================ -->
<record id="view_payslip_generate_wizard_form" model="ir.ui.view">
<field name="name">c2c.payslip.generate.wizard.form</field>
<field name="model">c2c.payslip.generate.wizard</field>
<field name="arch" type="xml">
<form string="Generate Payslips">
<group>
<field name="date_from"/>
<field name="date_to"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<footer>
<button name="action_generate_payslips" string="Generate"
type="object" class="oe_highlight"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_payslip_generate_wizard" model="ir.actions.act_window">
<field name="name">Generate Payslips</field>
<field name="res_model">c2c.payslip.generate.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Salary Structure — Tree View -->
<!-- ============================================================ -->
<record id="view_salary_structure_tree" model="ir.ui.view">
<field name="name">c2c.salary.structure.tree</field>
<field name="model">c2c.salary.structure</field>
<field name="arch" type="xml">
<tree string="Salary Structures">
<field name="name"/>
<field name="basic_percentage"/>
<field name="hra_percentage"/>
<field name="allowance_percentage"/>
<field name="pf_percentage"/>
<field name="esi_percentage"/>
<field name="professional_tax_fixed"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- ============================================================ -->
<!-- Salary Structure — Form View -->
<!-- ============================================================ -->
<record id="view_salary_structure_form" model="ir.ui.view">
<field name="name">c2c.salary.structure.form</field>
<field name="model">c2c.salary.structure</field>
<field name="arch" type="xml">
<form string="Salary Structure">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="e.g. Standard Structure"/>
</h1>
</div>
<group>
<group string="Earnings (%)">
<field name="basic_percentage"/>
<field name="hra_percentage"/>
<field name="allowance_percentage"/>
</group>
<group string="Deductions">
<field name="pf_percentage"/>
<field name="esi_percentage"/>
<field name="professional_tax_fixed"/>
</group>
</group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" invisible="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============================================================ -->
<!-- Salary Structure — Action -->
<!-- ============================================================ -->
<record id="action_salary_structure" model="ir.actions.act_window">
<field name="name">Salary Structures</field>
<field name="res_model">c2c.salary.structure</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first Salary Structure
</p>
<p>
Define how gross salary is split into Basic, HRA, and Allowances,
and configure PF, ESI, and Professional Tax deduction rules.
</p>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import payslip_generate_wizard

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.exceptions import UserError
class PayslipGenerateWizard(models.TransientModel):
_name = 'c2c.payslip.generate.wizard'
_description = 'Generate Payslips Wizard'
date_from = fields.Date(string='Period From', required=True)
date_to = fields.Date(string='Period To', required=True)
company_id = fields.Many2one(
'res.company', string='Company', required=True,
default=lambda self: self.env.company,
)
def action_generate_payslips(self):
"""Generate payslips for all employees with active contracts."""
self.ensure_one()
if self.date_from > self.date_to:
raise UserError('Period From cannot be after Period To.')
contracts = self.env['hr.contract'].search([
('state', '=', 'open'),
('company_id', '=', self.company_id.id),
('salary_structure_id', '!=', False),
('gross_salary', '>', 0),
])
if not contracts:
raise UserError(
'No active contracts found with a salary structure and gross salary '
'for company %s.' % self.company_id.name
)
Payslip = self.env['c2c.payslip']
created_payslips = Payslip
for contract in contracts:
# Skip if payslip already exists for this employee and period
existing = Payslip.search([
('employee_id', '=', contract.employee_id.id),
('date_from', '=', self.date_from),
('date_to', '=', self.date_to),
('company_id', '=', self.company_id.id),
], limit=1)
if existing:
continue
payslip = Payslip.create({
'employee_id': contract.employee_id.id,
'contract_id': contract.id,
'date_from': self.date_from,
'date_to': self.date_to,
'company_id': self.company_id.id,
})
created_payslips |= payslip
if not created_payslips:
raise UserError(
'All eligible employees already have payslips for the selected period.'
)
return {
'type': 'ir.actions.act_window',
'name': 'Generated Payslips',
'res_model': 'c2c.payslip',
'view_mode': 'tree,form',
'domain': [('id', 'in', created_payslips.ids)],
'target': 'current',
}

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
{
'name': 'Employee Documents',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Manage Employee Documents',
'description': """
This module allows you to manage employee documents such as Offer Letters, Contracts, ID Proofs, etc.
It adds a documents tab in the Employee form view.
""",
'author': 'Antigravity',
'depends': ['hr'],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'views/employee_document_views.xml',
'views/hr_employee_views.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,2 @@
from . import employee_document
from . import hr_employee

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from datetime import datetime
class EmployeeDocument(models.Model):
_name = 'employee.document'
_description = 'Employee Document'
name = fields.Char(string='Document Reference', required=True)
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
document_type = fields.Selection([
('offer_letter', 'Offer Letter'),
('contract', 'Contract'),
('id_proof', 'ID Proof'),
('other', 'Other'),
], string='Document Type', default='other', required=True)
file = fields.Binary(string='File', required=True)
file_name = fields.Char(string='File Name')
upload_date = fields.Datetime(string='Upload Date', default=fields.Datetime.now)

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class HrEmployee(models.Model):
_inherit = 'hr.employee'
document_ids = fields.One2many('employee.document', 'employee_id', string='Documents')

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_employee_document_user,employee.document.user,model_employee_document,base.group_user,1,0,0,0
access_employee_document_manager,employee.document.manager,model_employee_document,employee_documents.group_hr_documents,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_employee_document_user employee.document.user model_employee_document base.group_user 1 0 0 0
3 access_employee_document_manager employee.document.manager model_employee_document employee_documents.group_hr_documents 1 1 1 1

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="group_hr_documents" model="res.groups">
<field name="name">HR Documents</field>
<field name="category_id" ref="base.module_category_human_resources_employees"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_employee_document_tree" model="ir.ui.view">
<field name="name">employee.document.tree</field>
<field name="model">employee.document</field>
<field name="arch" type="xml">
<tree string="Employee Documents">
<field name="name"/>
<field name="employee_id"/>
<field name="document_type"/>
<field name="upload_date"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_employee_document_form" model="ir.ui.view">
<field name="name">employee.document.form</field>
<field name="model">employee.document</field>
<field name="arch" type="xml">
<form string="Employee Document">
<sheet>
<group>
<group>
<field name="name"/>
<field name="employee_id"/>
<field name="document_type"/>
</group>
<group>
<field name="upload_date"/>
<field name="file" filename="file_name"/>
<field name="file_name" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_employee_document_search" model="ir.ui.view">
<field name="name">employee.document.search</field>
<field name="model">employee.document</field>
<field name="arch" type="xml">
<search string="Search Employee Documents">
<field name="name"/>
<field name="employee_id"/>
<field name="document_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="group_by_employee" context="{'group_by':'employee_id'}"/>
<filter string="Document Type" name="group_by_type" context="{'group_by':'document_type'}"/>
</group>
</search>
</field>
</record>
<!-- Haction -->
<record id="action_employee_document" model="ir.actions.act_window">
<field name="name">Documents</field>
<field name="res_model">employee.document</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first employee document
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_employee_document"
name="Documents"
parent="hr.menu_hr_root"
action="action_employee_document"
sequence="5"/>
</odoo>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_employee_form_inherit_document" model="ir.ui.view">
<field name="name">hr.employee.form.inherit.document</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Documents" name="documents">
<field name="document_ids">
<tree editable="bottom">
<field name="name"/>
<field name="document_type"/>
<field name="upload_date"/>
<field name="file" filename="file_name"/>
<field name="file_name" invisible="1"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

33
docker-compose.yml Normal file
View File

@ -0,0 +1,33 @@
version: "3.8"
services:
db:
image: postgres:15
container_name: odoo_metatroncube_db
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
volumes:
- client1_pgdata:/var/lib/postgresql/data
restart: always
odoo:
image: odoo:17.0
container_name: odoo_metatroncube
depends_on:
- db
ports:
- "10101:8069"
environment:
HOST: db
USER: odoo
PASSWORD: odoo
volumes:
- client1_odoo_data:/var/lib/odoo
- ./addons:/mnt/extra-addons
restart: always
volumes:
client1_pgdata:
client1_odoo_data: