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

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 

6 

7 

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) 

18 

19 class Meta: 

20 constraints = [models.UniqueConstraint(fields=['tenant_id', 'employee_number'], name='hr_employee_number_tenant_unique')] 

21 ordering = ['employee_number'] 

22 

23 

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) 

31 

32 

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') 

41 

42 

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') 

50 

51 

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') 

60 

61 

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) 

72 

73 

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) 

82 

83 

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) 

92 

93 

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') 

101 

102 

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') 

130 

131 

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) 

147 

148 class Meta: 

149 constraints = [ 

150 models.UniqueConstraint(fields=['tenant_id', 'year'], name='hr_month13_year_tenant_unique') 

151 ] 

152 

153 

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) 

168 

169 class Meta: 

170 constraints = [ 

171 models.UniqueConstraint(fields=['bonus', 'profile'], name='hr_month13_item_unique') 

172 ]