Coverage for modules\hr\models.py: 100%
128 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 16:58 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 16:58 +0800
1from decimal import Decimal
2from django.core.validators import MinValueValidator
3from django.db import models
4from core.models import TenantScopedModel
5from modules.authentication.models import User
8class EmployeeProfile(TenantScopedModel):
9 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hr_profiles')
10 employee_number = models.CharField(max_length=32)
11 hire_date = models.DateField(null=True, blank=True)
12 birth_date = models.DateField(null=True, blank=True)
13 employment_status = models.CharField(max_length=32, default='ACTIVE')
14 job_title = models.CharField(max_length=128, blank=True)
15 salary_monthly = models.DecimalField(max_digits=14, decimal_places=2, default=0, validators=[MinValueValidator(0)])
16 government_ids = models.JSONField(default=dict, blank=True)
17 emergency_contact = models.JSONField(default=dict, blank=True)
19 class Meta:
20 constraints = [models.UniqueConstraint(fields=['tenant_id', 'employee_number'], name='hr_employee_number_tenant_unique')]
21 ordering = ['employee_number']
24class EmployeeRecord(TenantScopedModel):
25 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='records')
26 record_type = models.CharField(max_length=64)
27 title = models.CharField(max_length=160)
28 file_url = models.URLField(max_length=500, blank=True)
29 issued_on = models.DateField(null=True, blank=True)
30 notes = models.TextField(blank=True)
33class LeaveRequest(TenantScopedModel):
34 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='leave_requests')
35 leave_type = models.CharField(max_length=64)
36 start_date = models.DateField()
37 end_date = models.DateField()
38 reason = models.TextField(blank=True)
39 status = models.CharField(max_length=16, default='PENDING')
40 reviewed_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='reviewed_leave_requests')
43class OvertimeRequest(TenantScopedModel):
44 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='overtime_requests')
45 date = models.DateField()
46 hours = models.DecimalField(max_digits=4, decimal_places=2, validators=[MinValueValidator(Decimal('0.5'))])
47 reason = models.TextField(blank=True)
48 status = models.CharField(max_length=16, default='PENDING')
49 reviewed_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='reviewed_overtime_requests')
52class Claim(TenantScopedModel):
53 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='claims')
54 category = models.CharField(max_length=64)
55 amount = models.DecimalField(max_digits=14, decimal_places=2, validators=[MinValueValidator(0)])
56 claim_date = models.DateField()
57 description = models.TextField(blank=True)
58 status = models.CharField(max_length=16, default='PENDING')
59 reviewed_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='reviewed_claims')
62class PayrollRun(TenantScopedModel):
63 period_start = models.DateField()
64 period_end = models.DateField()
65 status = models.CharField(max_length=16, default='DRAFT')
66 total_gross = models.DecimalField(max_digits=16, decimal_places=2, default=0)
67 total_tax = models.DecimalField(max_digits=16, decimal_places=2, default=0)
68 total_net = models.DecimalField(max_digits=16, decimal_places=2, default=0)
69 sss_contribution = models.DecimalField(max_digits=16, decimal_places=2, default=0)
70 philhealth_contribution = models.DecimalField(max_digits=16, decimal_places=2, default=0)
71 pagibig_contribution = models.DecimalField(max_digits=16, decimal_places=2, default=0)
74class PayrollItem(TenantScopedModel):
75 run = models.ForeignKey(PayrollRun, on_delete=models.CASCADE, related_name='items')
76 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='payroll_items')
77 pay_rates = models.JSONField(default=list)
78 gross_pay = models.DecimalField(max_digits=16, decimal_places=2, default=0)
79 taxable_income = models.DecimalField(max_digits=16, decimal_places=2, default=0)
80 withholding_tax = models.DecimalField(max_digits=16, decimal_places=2, default=0)
81 net_pay = models.DecimalField(max_digits=16, decimal_places=2, default=0)
84class TaxDocument(TenantScopedModel):
85 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='tax_documents')
86 tax_year = models.PositiveIntegerField()
87 document_type = models.CharField(max_length=32, default='BIR_2316')
88 status = models.CharField(max_length=32, default='DRAFT')
89 annual_gross = models.DecimalField(max_digits=16, decimal_places=2, default=0)
90 annual_tax = models.DecimalField(max_digits=16, decimal_places=2, default=0)
91 file_url = models.URLField(max_length=500, blank=True)
94class PerformanceReview(TenantScopedModel):
95 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='performance_reviews')
96 review_period = models.CharField(max_length=64)
97 rating = models.DecimalField(max_digits=4, decimal_places=2, null=True, blank=True)
98 goals = models.JSONField(default=list, blank=True)
99 feedback = models.TextField(blank=True)
100 status = models.CharField(max_length=32, default='DRAFT')
103class Payslip(TenantScopedModel):
104 """Individual payslip per payroll item."""
105 payroll_item = models.OneToOneField(PayrollItem, on_delete=models.CASCADE, related_name='payslip')
106 employee_name = models.CharField(max_length=256)
107 employee_number = models.CharField(max_length=32)
108 job_title = models.CharField(max_length=128, blank=True)
109 period_start = models.DateField()
110 period_end = models.DateField()
111 # Earnings breakdown
112 basic_salary = models.DecimalField(max_digits=14, decimal_places=2, default=0)
113 overtime_pay = models.DecimalField(max_digits=14, decimal_places=2, default=0)
114 allowances = models.DecimalField(max_digits=14, decimal_places=2, default=0)
115 other_earnings = models.DecimalField(max_digits=14, decimal_places=2, default=0)
116 gross_pay = models.DecimalField(max_digits=14, decimal_places=2, default=0)
117 # Deductions
118 sss_deduction = models.DecimalField(max_digits=14, decimal_places=2, default=0)
119 philhealth_deduction = models.DecimalField(max_digits=14, decimal_places=2, default=0)
120 pagibig_deduction = models.DecimalField(max_digits=14, decimal_places=2, default=0)
121 withholding_tax = models.DecimalField(max_digits=14, decimal_places=2, default=0)
122 other_deductions = models.DecimalField(max_digits=14, decimal_places=2, default=0)
123 total_deductions = models.DecimalField(max_digits=14, decimal_places=2, default=0)
124 # Net pay
125 net_pay = models.DecimalField(max_digits=14, decimal_places=2, default=0)
126 status = models.CharField(max_length=16, default='GENERATED') # GENERATED, RELEASED, CANCELLED
127 generated_at = models.DateTimeField(auto_now_add=True)
128 released_at = models.DateTimeField(null=True, blank=True)
129 reviewer = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='performance_reviews_given')
132class Month13Bonus(TenantScopedModel):
133 """
134 13th Month Bonus — mandated Philippine benefit.
135 One record per employee per year. Calculated as:
136 total basic salary for the year / 12
137 Exempt from withholding tax (BIR-compliant).
138 """
139 year = models.PositiveIntegerField()
140 status = models.CharField(max_length=16, default='DRAFT') # DRAFT, RELEASED, PAID
141 total_gross = models.DecimalField(max_digits=16, decimal_places=2, default=0)
142 total_net = models.DecimalField(max_digits=16, decimal_places=2, default=0) # same as total_gross (tax-exempt)
143 item_count = models.PositiveIntegerField(default=0)
144 notes = models.TextField(blank=True)
145 created_at = models.DateTimeField(auto_now_add=True)
146 updated_at = models.DateTimeField(auto_now=True)
148 class Meta:
149 constraints = [
150 models.UniqueConstraint(fields=['tenant_id', 'year'], name='hr_month13_year_tenant_unique')
151 ]
154class Month13Item(TenantScopedModel):
155 """
156 Individual 13th month bonus line per employee.
157 """
158 bonus = models.ForeignKey(Month13Bonus, on_delete=models.CASCADE, related_name='items')
159 profile = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='month13_items')
160 employee_name = models.CharField(max_length=256)
161 employee_number = models.CharField(max_length=32)
162 months_employed = models.PositiveIntegerField(default=12) # number of months worked in the year
163 months_employed_json = models.JSONField(default=list, blank=True) # list of month numbers worked
164 basic_salary_total = models.DecimalField(max_digits=16, decimal_places=2, default=0) # total basic salary for the year
165 bonus_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0) # basic_salary_total / 12
166 status = models.CharField(max_length=16, default='PENDING') # PENDING, RELEASED, PAID
167 released_at = models.DateTimeField(null=True, blank=True)
169 class Meta:
170 constraints = [
171 models.UniqueConstraint(fields=['bonus', 'profile'], name='hr_month13_item_unique')
172 ]