Ver 14.06 Overtime working
This commit is contained in:
parent
bb49956693
commit
f3ada22ab3
Binary file not shown.
Binary file not shown.
19
core/migrations/0015_payrolladjustment_work_log.py
Normal file
19
core/migrations/0015_payrolladjustment_work_log.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-10 11:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0014_remove_worklog_overtime_priced_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payrolladjustment',
|
||||||
|
name='work_log',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments', to='core.worklog'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -145,6 +145,9 @@ class PayrollAdjustment(models.Model):
|
|||||||
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments')
|
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments')
|
||||||
payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
|
payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
|
||||||
loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments')
|
loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments')
|
||||||
|
# Link back to WorkLog to track Project/Team context for Overtime
|
||||||
|
work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
|
||||||
|
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Positive adds to pay, negative subtracts (except for Loan Repayment which is auto-handled)")
|
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Positive adds to pay, negative subtracts (except for Loan Repayment which is auto-handled)")
|
||||||
date = models.DateField(default=timezone.now)
|
date = models.DateField(default=timezone.now)
|
||||||
description = models.CharField(max_length=255)
|
description = models.CharField(max_length=255)
|
||||||
@ -201,4 +204,4 @@ class ExpenseLineItem(models.Model):
|
|||||||
verbose_name_plural = "Expense Line Items"
|
verbose_name_plural = "Expense Line Items"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.product} - {self.amount}"
|
return f"{self.product} - {self.amount}"
|
||||||
|
|||||||
130
core/views.py
130
core/views.py
@ -63,10 +63,23 @@ def home(request):
|
|||||||
|
|
||||||
if user_is_admin:
|
if user_is_admin:
|
||||||
# 1. Outstanding Payments
|
# 1. Outstanding Payments
|
||||||
active_workers = Worker.objects.filter(is_active=True)
|
active_workers = Worker.objects.filter(is_active=True).prefetch_related('work_logs', 'adjustments')
|
||||||
for worker in active_workers:
|
for worker in active_workers:
|
||||||
|
# Unpaid logs
|
||||||
unpaid_logs_count = worker.work_logs.exclude(paid_in__worker=worker).count()
|
unpaid_logs_count = worker.work_logs.exclude(paid_in__worker=worker).count()
|
||||||
outstanding_total += unpaid_logs_count * worker.day_rate
|
log_amount = unpaid_logs_count * worker.day_rate
|
||||||
|
|
||||||
|
# Pending Adjustments
|
||||||
|
pending_adjustments = worker.adjustments.filter(payroll_record__isnull=True)
|
||||||
|
adj_total = Decimal('0.00')
|
||||||
|
for adj in pending_adjustments:
|
||||||
|
if adj.type in ['BONUS', 'OVERTIME', 'LOAN']:
|
||||||
|
adj_total += adj.amount
|
||||||
|
elif adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']:
|
||||||
|
adj_total -= adj.amount
|
||||||
|
|
||||||
|
total_payable = log_amount + adj_total
|
||||||
|
outstanding_total += max(total_payable, Decimal('0.00'))
|
||||||
|
|
||||||
# 2. Paid This Month
|
# 2. Paid This Month
|
||||||
recent_payments_total = PayrollRecord.objects.filter(
|
recent_payments_total = PayrollRecord.objects.filter(
|
||||||
@ -298,22 +311,22 @@ def work_log_list(request):
|
|||||||
logs = logs.filter(paid_in__isnull=True)
|
logs = logs.filter(paid_in__isnull=True)
|
||||||
|
|
||||||
# --- 2. Fetch Adjustments ---
|
# --- 2. Fetch Adjustments ---
|
||||||
# Adjustments are shown unless a Project/Team filter is active (as they don't belong to projects/teams),
|
adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log')
|
||||||
# OR if a specific worker is selected (then we always show their adjustments).
|
|
||||||
show_adjustments = True
|
|
||||||
if (project_id or team_id) and not worker_id:
|
|
||||||
show_adjustments = False
|
|
||||||
|
|
||||||
adjustments = PayrollAdjustment.objects.none()
|
if worker_id:
|
||||||
if show_adjustments:
|
adjustments = adjustments.filter(worker_id=worker_id)
|
||||||
adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record')
|
|
||||||
if worker_id:
|
if project_id:
|
||||||
adjustments = adjustments.filter(worker_id=worker_id)
|
# Include only adjustments linked to this project (via work_log)
|
||||||
|
adjustments = adjustments.filter(work_log__project_id=project_id)
|
||||||
|
|
||||||
if payment_status == 'paid':
|
if team_id:
|
||||||
adjustments = adjustments.filter(payroll_record__isnull=False)
|
adjustments = adjustments.filter(work_log__team_id=team_id)
|
||||||
elif payment_status == 'unpaid':
|
|
||||||
adjustments = adjustments.filter(payroll_record__isnull=True)
|
if payment_status == 'paid':
|
||||||
|
adjustments = adjustments.filter(payroll_record__isnull=False)
|
||||||
|
elif payment_status == 'unpaid':
|
||||||
|
adjustments = adjustments.filter(payroll_record__isnull=True)
|
||||||
|
|
||||||
# --- 3. Date Filtering for Calendar View (Applied to both) ---
|
# --- 3. Date Filtering for Calendar View (Applied to both) ---
|
||||||
start_date = None
|
start_date = None
|
||||||
@ -338,8 +351,8 @@ def work_log_list(request):
|
|||||||
end_date = datetime.date(curr_year, curr_month, num_days)
|
end_date = datetime.date(curr_year, curr_month, num_days)
|
||||||
|
|
||||||
logs = logs.filter(date__range=(start_date, end_date))
|
logs = logs.filter(date__range=(start_date, end_date))
|
||||||
if show_adjustments:
|
# No 'show_adjustments' check needed as query is already filtered
|
||||||
adjustments = adjustments.filter(date__range=(start_date, end_date))
|
adjustments = adjustments.filter(date__range=(start_date, end_date))
|
||||||
|
|
||||||
# --- 4. Combine and Sort ---
|
# --- 4. Combine and Sort ---
|
||||||
user_is_admin = is_admin(request.user)
|
user_is_admin = is_admin(request.user)
|
||||||
@ -376,32 +389,31 @@ def work_log_list(request):
|
|||||||
combined_records.append(record)
|
combined_records.append(record)
|
||||||
|
|
||||||
# Process Adjustments
|
# Process Adjustments
|
||||||
if show_adjustments:
|
for adj in adjustments:
|
||||||
for adj in adjustments:
|
# Determine signed amount for display/total
|
||||||
# Determine signed amount for display/total
|
amt = adj.amount
|
||||||
amt = adj.amount
|
if adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']:
|
||||||
if adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']:
|
amt = -amt
|
||||||
amt = -amt
|
|
||||||
|
record = {
|
||||||
|
'type': 'ADJ',
|
||||||
|
'date': adj.date,
|
||||||
|
'obj': adj,
|
||||||
|
'project_name': f"{adj.get_type_display()}", # Use project column for Type
|
||||||
|
'team_name': None,
|
||||||
|
'workers': [adj.worker],
|
||||||
|
'supervisor': "System",
|
||||||
|
'is_paid': adj.payroll_record is not None,
|
||||||
|
'paid_record': adj.payroll_record,
|
||||||
|
'notes': adj.description,
|
||||||
|
'amount': amt if user_is_admin else None,
|
||||||
|
'sort_id': adj.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_is_admin:
|
||||||
|
total_amount += amt
|
||||||
|
|
||||||
record = {
|
combined_records.append(record)
|
||||||
'type': 'ADJ',
|
|
||||||
'date': adj.date,
|
|
||||||
'obj': adj,
|
|
||||||
'project_name': f"{adj.get_type_display()}", # Use project column for Type
|
|
||||||
'team_name': None,
|
|
||||||
'workers': [adj.worker],
|
|
||||||
'supervisor': "System",
|
|
||||||
'is_paid': adj.payroll_record is not None,
|
|
||||||
'paid_record': adj.payroll_record,
|
|
||||||
'notes': adj.description,
|
|
||||||
'amount': amt if user_is_admin else None,
|
|
||||||
'sort_id': adj.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_is_admin:
|
|
||||||
total_amount += amt
|
|
||||||
|
|
||||||
combined_records.append(record)
|
|
||||||
|
|
||||||
# Sort combined list by Date Descending, then ID Descending
|
# Sort combined list by Date Descending, then ID Descending
|
||||||
combined_records.sort(key=lambda x: (x['date'], x['sort_id']), reverse=True)
|
combined_records.sort(key=lambda x: (x['date'], x['sort_id']), reverse=True)
|
||||||
@ -522,20 +534,23 @@ def export_work_log_csv(request):
|
|||||||
logs = logs.filter(paid_in__isnull=True)
|
logs = logs.filter(paid_in__isnull=True)
|
||||||
|
|
||||||
# --- 2. Fetch Adjustments ---
|
# --- 2. Fetch Adjustments ---
|
||||||
show_adjustments = True
|
adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log')
|
||||||
if (project_id or team_id) and not worker_id:
|
|
||||||
show_adjustments = False
|
|
||||||
|
|
||||||
adjustments = []
|
if worker_id:
|
||||||
if show_adjustments:
|
adjustments = adjustments.filter(worker_id=worker_id)
|
||||||
qs = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record')
|
|
||||||
if worker_id:
|
if project_id:
|
||||||
qs = qs.filter(worker_id=worker_id)
|
adjustments = adjustments.filter(work_log__project_id=project_id)
|
||||||
if payment_status == 'paid':
|
|
||||||
qs = qs.filter(payroll_record__isnull=False)
|
if team_id:
|
||||||
elif payment_status == 'unpaid':
|
adjustments = adjustments.filter(work_log__team_id=team_id)
|
||||||
qs = qs.filter(payroll_record__isnull=True)
|
|
||||||
adjustments = list(qs)
|
if payment_status == 'paid':
|
||||||
|
adjustments = adjustments.filter(payroll_record__isnull=False)
|
||||||
|
elif payment_status == 'unpaid':
|
||||||
|
adjustments = adjustments.filter(payroll_record__isnull=True)
|
||||||
|
|
||||||
|
adjustments = list(adjustments)
|
||||||
|
|
||||||
user_is_admin = is_admin(request.user)
|
user_is_admin = is_admin(request.user)
|
||||||
|
|
||||||
@ -982,7 +997,8 @@ def price_overtime(request):
|
|||||||
type='OVERTIME',
|
type='OVERTIME',
|
||||||
amount=amount,
|
amount=amount,
|
||||||
date=worklog.date,
|
date=worklog.date,
|
||||||
description=f"Overtime: {worklog.get_overtime_display()} @ {rate_pct}% - {worklog.date.strftime('%d %b %Y')}"
|
description=f"Overtime: {worklog.get_overtime_display()} @ {rate_pct}% - {worklog.date.strftime('%d %b %Y')}",
|
||||||
|
work_log=worklog
|
||||||
)
|
)
|
||||||
created += 1
|
created += 1
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user