From c88e3441985bf1bf55606f6bd4ca02d0ca1d4e14 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Fri, 20 Mar 2026 13:07:28 -0500 Subject: [PATCH] Implemented financial tracking --- .antigravityrules | 4 + .cursorrules | 4 + company_site/financial/forms.py | 82 ++++++- .../0006_chargenumber_created_by_and_more.py | 76 +++++++ ...contract_type_contracttypeenum_and_more.py | 34 +++ ...umber_slug_alter_contract_slug_and_more.py | 38 ++++ ...rdcell_end_time_timecardcell_start_time.py | 23 ++ ...oyee_manager_alter_employee_phonenumber.py | 25 +++ company_site/financial/models.py | 39 +++- .../templates/financial/contract_detail.html | 95 ++++---- .../templates/financial/contracts.html | 189 ++++++++++++---- .../templates/financial/edit_time_log.html | 24 ++ .../financial/templates/financial/index.html | 205 ++++++++--------- .../templates/financial/new_employee.html | 25 +++ .../templates/financial/not_created.html | 14 +- .../templates/financial/profile.html | 31 +-- .../templates/financial/reports.html | 44 ++++ .../templates/financial/time_logs.html | 64 ++++++ .../templates/financial/timekeeping.html | 31 +++ company_site/financial/urls.py | 5 + company_site/financial/views.py | 211 ++++++++++++++---- .../public/static/public/css/style.css | 65 ++++++ 22 files changed, 1058 insertions(+), 270 deletions(-) create mode 100644 .antigravityrules create mode 100644 .cursorrules create mode 100644 company_site/financial/migrations/0006_chargenumber_created_by_and_more.py create mode 100644 company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py create mode 100644 company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py create mode 100644 company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py create mode 100644 company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py create mode 100644 company_site/financial/templates/financial/edit_time_log.html create mode 100644 company_site/financial/templates/financial/new_employee.html create mode 100644 company_site/financial/templates/financial/reports.html create mode 100644 company_site/financial/templates/financial/time_logs.html create mode 100644 company_site/financial/templates/financial/timekeeping.html diff --git a/.antigravityrules b/.antigravityrules new file mode 100644 index 0000000..3138ce4 --- /dev/null +++ b/.antigravityrules @@ -0,0 +1,4 @@ +# Django Virtual Environment Rule + +Always use the virtual environment located at `.venv` when running any Python scripts, pip commands, or Django management commands. +You can run commands using the virtual environment's Python executable directly (e.g., `.venv/bin/python manage.py ...`) or activate it first. diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..3138ce4 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,4 @@ +# Django Virtual Environment Rule + +Always use the virtual environment located at `.venv` when running any Python scripts, pip commands, or Django management commands. +You can run commands using the virtual environment's Python executable directly (e.g., `.venv/bin/python manage.py ...`) or activate it first. diff --git a/company_site/financial/forms.py b/company_site/financial/forms.py index 1d5a277..7d8968c 100644 --- a/company_site/financial/forms.py +++ b/company_site/financial/forms.py @@ -1,20 +1,90 @@ +import datetime from django import forms - from django.forms import ModelForm -from .models import Employee, Contract, ChargeNumber +from .models import Employee, Contract, ChargeNumber, TimeCardCell, AddressModel + +class NewEmployeeForm(ModelForm): + first_name = forms.CharField(max_length=30, required=False, label="First Name") + last_name = forms.CharField(max_length=30, required=False, label="Last Name") + address_1 = forms.CharField(max_length=128, label="Address Line 1") + address_2 = forms.CharField(max_length=128, required=False, label="Address Line 2") + city = forms.CharField(max_length=64, label="City") + state = forms.CharField(max_length=2, label="State (e.g. NY)") + zip_code = forms.CharField(max_length=5, label="ZIP Code") + + class Meta: + model = Employee + fields = ["user", "manager", "phoneNumber", "slary"] + + def save(self, commit=True): + employee = super().save(commit=False) + + if self.cleaned_data.get('first_name') or self.cleaned_data.get('last_name'): + if self.cleaned_data.get('first_name'): + employee.user.first_name = self.cleaned_data.get('first_name') + if self.cleaned_data.get('last_name'): + employee.user.last_name = self.cleaned_data.get('last_name') + employee.user.save() + + address = AddressModel.objects.create( + address_1=self.cleaned_data['address_1'], + address_2=self.cleaned_data['address_2'], + city=self.cleaned_data['city'], + state=self.cleaned_data['state'], + zip_code=self.cleaned_data['zip_code'], + ) + employee.primaryAddress = address + employee.workAddress = address + if commit: + employee.save() + return employee class EmployeeForm(ModelForm): class Meta: model = Employee - # TODO: fix slary to be salary - fields = ["primaryAddress","workAddress", "slary"] + fields = ["user", "manager", "primaryAddress", "workAddress", "phoneNumber", "slary"] class ContractForm(ModelForm): class Meta: model = Contract - fields = ["contract_type","name","proposed_amount","baseline_amount","funded_amount","baseline_start","baseline_end"] + fields = ["contract_type","name","proposed_amount","baseline_amount","funded_amount","budget_hours","baseline_start","baseline_end"] class ChargeNumberForm(ModelForm): class Meta: model = ChargeNumber - fields = ["charge_number_type","amount", "start_date","end_date"] \ No newline at end of file + fields = ["charge_number_type","amount", "start_date","end_date"] + +class TimeLogForm(ModelForm): + start_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'})) + end_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'})) + hour = forms.FloatField(required=False, label="Duration (hours)") + date = forms.DateField(initial=datetime.date.today, widget=forms.DateInput(attrs={'type': 'date'})) + + class Meta: + model = TimeCardCell + fields = ["contract", "date", "start_time", "end_time", "hour"] + + def clean(self): + cleaned_data = super().clean() + start = cleaned_data.get('start_time') + end = cleaned_data.get('end_time') + duration = cleaned_data.get('hour') + + if start and end: + dt_start = datetime.datetime.combine(datetime.date.today(), start) + dt_end = datetime.datetime.combine(datetime.date.today(), end) + diff = (dt_end - dt_start).total_seconds() / 3600.0 + if diff < 0: + diff += 24.0 + cleaned_data['hour'] = round(diff, 2) + elif start and duration: + dt_start = datetime.datetime.combine(datetime.date.today(), start) + dt_end = dt_start + datetime.timedelta(hours=duration) + cleaned_data['end_time'] = (dt_end).time() + elif not duration and not (start and end): + raise forms.ValidationError("You must provide either (Start Time and End Time) OR (Start Time and Duration) OR (Duration).") + + if not cleaned_data.get('hour') and duration: + cleaned_data['hour'] = duration + + return cleaned_data \ No newline at end of file diff --git a/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py b/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py new file mode 100644 index 0000000..eee5efe --- /dev/null +++ b/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 5.0 on 2026-03-20 11:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financial', '0005_chargenumber_financial_chargenumber_charge_number_type_chargenumbertypeenum_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='chargenumber', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='chargenumber', + name='last_modified_BY', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contract', + name='budget_hours', + field=models.FloatField(default=0.0), + ), + migrations.AddField( + model_name='contract', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contract', + name='last_modified_BY', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='employee', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='employee', + name='last_modified_BY', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='timecard', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='timecard', + name='last_modified_BY', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='timecardcell', + name='contract', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='financial.contract'), + ), + migrations.AddField( + model_name='timecardcell', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='timecardcell', + name='last_modified_BY', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py b/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py new file mode 100644 index 0000000..abb4c0c --- /dev/null +++ b/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0 on 2026-03-20 14:35 + +import django_enum.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financial', '0006_chargenumber_created_by_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='contract', + name='financial_Contract_contract_type_ContractTypeEnum', + ), + migrations.AddField( + model_name='contract', + name='award_amount', + field=models.FloatField(default=0.0), + ), + migrations.AlterField( + model_name='contract', + name='contract_type', + field=django_enum.fields.EnumCharField(choices=[('FFP', 'FIRM_FIX_PRICED'), ('CPFF', 'COST_PLUS_FIXED_FEE'), ('MAX', 'MAX_NUM_CONTRACT_TYPES')], max_length=4), + ), + migrations.AddConstraint( + model_name='contract', + constraint=models.CheckConstraint(check=models.Q(('contract_type__in', ['FFP', 'CPFF', 'MAX'])), name='financial_Contract_contract_type_ContractTypeEnum'), + ), + ] diff --git a/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py b/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py new file mode 100644 index 0000000..9cfe100 --- /dev/null +++ b/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0 on 2026-03-20 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financial', '0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='chargenumber', + name='slug', + field=models.SlugField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='contract', + name='slug', + field=models.SlugField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='employee', + name='slug', + field=models.SlugField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='timecard', + name='slug', + field=models.SlugField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name='timecardcell', + name='slug', + field=models.SlugField(blank=True, null=True, unique=True), + ), + ] diff --git a/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py b/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py new file mode 100644 index 0000000..cc11e60 --- /dev/null +++ b/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0 on 2026-03-20 15:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financial', '0008_alter_chargenumber_slug_alter_contract_slug_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='timecardcell', + name='end_time', + field=models.TimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='timecardcell', + name='start_time', + field=models.TimeField(blank=True, null=True), + ), + ] diff --git a/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py b/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py new file mode 100644 index 0000000..2caa783 --- /dev/null +++ b/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0 on 2026-03-20 15:30 + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financial', '0009_timecardcell_end_time_timecardcell_start_time'), + ] + + operations = [ + migrations.AlterField( + model_name='employee', + name='manager', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manager_employee', to='financial.employee'), + ), + migrations.AlterField( + model_name='employee', + name='phoneNumber', + field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True), + ), + ] diff --git a/company_site/financial/models.py b/company_site/financial/models.py index 63477de..6c51cec 100644 --- a/company_site/financial/models.py +++ b/company_site/financial/models.py @@ -6,25 +6,36 @@ from django_enum import EnumField from django.utils.text import slugify import datetime +def user_str(self): + if self.first_name and self.last_name: + return f"{self.first_name} {self.last_name}" + if self.username: + return self.username + if self.email: + return self.email + return str(self.id) + +User.__str__ = user_str + # Abstract Model Classes class TimeMixin(models.Model): created = models.DateTimeField(default=timezone.now) last_modified = models.DateTimeField(default=timezone.now) - created_by = models.ForeignKey - last_modified_BY = models.ForeignKey + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='+') + last_modified_BY = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='+') class Meta: abstract = True class IdMixin(models.Model): - slug = models.SlugField() + slug = models.SlugField(blank=True, unique=True, null=True) class Meta: abstract = True def save(self, *args, **kwargs): - if self.slug is None: + if not self.slug: self.slug = slugify(datetime.datetime.now().time()) super(IdMixin, self).save(*args, **kwargs) @@ -32,6 +43,7 @@ class IdMixin(models.Model): class Contract(IdMixin, TimeMixin): class ContractTypeEnum(models.TextChoices): FIRM_FIX_PRICED = "FFP", "FIRM_FIX_PRICED" + COST_PLUS_FIXED_FEE = "CPFF", "COST_PLUS_FIXED_FEE" MAX_NUM_CONRACT_TYPES = "MAX", "MAX_NUM_CONTRACT_TYPES" contract_type = EnumField(ContractTypeEnum) @@ -40,11 +52,16 @@ class Contract(IdMixin, TimeMixin): proposed_amount = models.FloatField(default=0.0) baseline_amount = models.FloatField(default=0.0) funded_amount = models.FloatField(default=0.0) + award_amount = models.FloatField(default=0.0) + budget_hours = models.FloatField(default=0.0) baseline_start = models.DateField(null=True, blank=True, default=None) baseline_end = models.DateField(null=True, blank=True, default=None) linkedContracts = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True) + def __str__(self): + return f"{self.name} - {self.contract_type}" + # TODO: make calc ev func class ChargeNumber(IdMixin, TimeMixin): @@ -73,14 +90,19 @@ class AddressModel(models.Model): zip_code = models.CharField(max_length=5) class Employee(IdMixin, TimeMixin): - manager = models.ForeignKey("self", on_delete=models.CASCADE, related_name="manager_employee") + manager = models.ForeignKey("self", on_delete=models.CASCADE, related_name="manager_employee", null=True, blank=True) user = models.OneToOneField(User, on_delete=models.CASCADE) primaryAddress = models.ForeignKey(AddressModel, on_delete=models.CASCADE, related_name="primary_address_employee") workAddress = models.ForeignKey(AddressModel, on_delete=models.CASCADE, related_name="work_address_employee") - phoneNumber = PhoneNumberField(null=False, blank=False, unique=True) + phoneNumber = PhoneNumberField(null=True, blank=True, unique=True) slary= models.FloatField(default=0.0) - # TODO: get hourly salary + def __str__(self): + return str(self.user) + + @property + def hourly_salary(self): + return (self.slary or 0.0) / 2080.0 # TODO: roles, jpbTitles @@ -97,7 +119,10 @@ class TimeCard(IdMixin, TimeMixin): class TimeCardCell(IdMixin, TimeMixin): timeCard = models.ForeignKey(TimeCard, on_delete=models.CASCADE) date = models.DateField(null=True, default = None) + start_time = models.TimeField(null=True, blank=True) + end_time = models.TimeField(null=True, blank=True) hour = models.FloatField(default = 0.0) + contract = models.ForeignKey(Contract, on_delete=models.CASCADE, null=True, blank=True) diff --git a/company_site/financial/templates/financial/contract_detail.html b/company_site/financial/templates/financial/contract_detail.html index d403277..f18b4c0 100644 --- a/company_site/financial/templates/financial/contract_detail.html +++ b/company_site/financial/templates/financial/contract_detail.html @@ -1,56 +1,65 @@ - - - +{% extends "base.html" %} {% load static %} - - - Contract Detail - - - +{% block title %}Contract Detail - AI ML Operations{% endblock %} -
+{% block content %} +
+
+
+ Back to + Contracts +
{% if is_new %} -

New Contract

-
- {% csrf_token %} - {{ form }} - -
+

New Contract

+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
{% else %} -

{{ contract.name }}

-
- {% csrf_token %} - {{ form }} - -
- {% endif %} +

{{ contract.name }}

+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
- - - - - - {% if is_new %} - {% else %} -

Charge Numbers

+

Charge Numbers

{% if charge_numbes %} -

put charge number table here

-

Create a new charge number

- {{ charge_number_form }} +
+

put charge number table here

+
+ +
+

Create a new charge number

+
+ {% csrf_token %} + {{ charge_number_form.as_p }} + +
+
{% else %} -

There are no charge numbers for this contract

-
- {% csrf_token %} - {{ charge_number_form }} - -
- {% endif %} +
+

There are no charge numbers for this contract. +

+
+ {% csrf_token %} + {{ charge_number_form.as_p }} + +
+
{% endif %} - - \ No newline at end of file + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/contracts.html b/company_site/financial/templates/financial/contracts.html index d4fcfa7..d0c151d 100644 --- a/company_site/financial/templates/financial/contracts.html +++ b/company_site/financial/templates/financial/contracts.html @@ -1,50 +1,151 @@ - - - +{% extends "base.html" %} {% load static %} - - - Profile - - - +{% block title %}Contracts - AI ML Operations{% endblock %} -
- - -

Contracts

- {% if contracts %} - - - - - - - - - - - - - {% for contract in contracts %} - - - - - - - - - - +{% block content %} +
+
+
+

Contracts

+ Back to + Dashboard +
+ {% if chart_data_json %} +

Burn Rate Charts

+
+ {% for c in contracts %} +
+ +
{% endfor %} -
NameIdentifierTypeStart DateEnd DateProposed AmountBaseline AmountFunded Amount
{{ contract.name }} {{ contract.slug }} {{ contract.contract_type }} {{ contract.baseline_start }} {{ contract.baseline_end }} {{ contract.proposed_amount }} {{ contract.baseline_amount }} {{ contract.funded_amount }}
-
-

Create new contract.

- {% else %} -

There are no contracts. Please make one

- + {% endif %} - \ No newline at end of file + +
+ {% if contracts %} + + + + + + + + + + + + + + + + + + {% for contract in contracts %} + + + + + + + + + + + + + + {% endfor %} + +
NameIdentifierTypeStart DateEnd DateProposed AmountBudget HoursHours ChargedMoney SpentMoney RemainingProjected End Date
{{ contract.name }}{{ contract.slug }}{{ contract.contract_type }}{{ contract.baseline_start }}{{ contract.baseline_end }}{{ contract.proposed_amount }}{{ contract.budget_hours }}{{ contract.total_hours_spent }}${{ contract.total_money_spent|floatformat:2 }}${{ contract.remaining_money|floatformat:2 }}{{ contract.projected_end_date }}
+ +

Create a new contract.

+ {% else %} +

There are no contracts. Please make one.

+ {% endif %} +
+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/edit_time_log.html b/company_site/financial/templates/financial/edit_time_log.html new file mode 100644 index 0000000..cdfe4ca --- /dev/null +++ b/company_site/financial/templates/financial/edit_time_log.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Edit Time Log - AI ML Operations{% endblock %} + +{% block content %} +
+
+

Edit Time Log

+ +
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Cancel +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/index.html b/company_site/financial/templates/financial/index.html index 39fbef7..15467f3 100644 --- a/company_site/financial/templates/financial/index.html +++ b/company_site/financial/templates/financial/index.html @@ -1,118 +1,95 @@ - +{% extends "base.html" %} {% load static %} - - - Accounting - - - -
- +{% block content %} +
+
+

Dashboard

+ + + +

Contracts Overview

+
+ {% if contracts %} + + + + + + + + + + {% for c in contracts %} + + + + + + {% endfor %} + +
Contract NameBudget HoursLogged Hours
{{ c.name }}{{ c.budget_hours }}{{ c.total_logged }}
+ {% else %} +

No contracts available.

+ {% endif %} +
+ +
+ +

Employee Hours per Contract

+
+ {% if employee_data %} + + + + + {% for c in contracts %} + + {% endfor %} + + + + {% for row in employee_data %} + + + {% for ch in row.contract_hours %} + + {% endfor %} + + {% endfor %} + +
Employee Name{{ c.name }}
{{ row.employee }}{{ ch.hours }}
+ {% else %} +

No employees available.

+ {% endif %} +
-
- -
-
-
-
- - -
-

put picture here

-
-
-
- {% if is_worker %} -
-
- - -
-

put picture here

-
-
-
- {% endif %} - {% if is_worker %} -
- -

put picture here

-
- - - {% endif %} - {% if is_manager %} -
-
- -
-

put picture here

-
-
-
- {% endif %} - {% if is_procurment_officer %} -
-
-
-
-
-

put picture here

-
-
-
- {% endif %} - {% if is_finance %} -
-
- -
-

put picture here

-
-
-
- {% endif %} - - -
-

Accouting

- \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/new_employee.html b/company_site/financial/templates/financial/new_employee.html new file mode 100644 index 0000000..1176803 --- /dev/null +++ b/company_site/financial/templates/financial/new_employee.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}New Employee - AI ML Operations{% endblock %} + +{% block content %} +
+
+
+ Back to + Dashboard +
+ +

New Employee

+ +
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/not_created.html b/company_site/financial/templates/financial/not_created.html index fad4fcc..68c883b 100644 --- a/company_site/financial/templates/financial/not_created.html +++ b/company_site/financial/templates/financial/not_created.html @@ -1 +1,13 @@ -

this page has not been created yet

\ No newline at end of file +{% extends "base.html" %} + +{% block title %}Page Not Found{% endblock %} + +{% block content %} +
+
+

Coming Soon

+

This page has not been created yet.

+ Return to Dashboard +
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/profile.html b/company_site/financial/templates/financial/profile.html index 3960525..2eb37a5 100644 --- a/company_site/financial/templates/financial/profile.html +++ b/company_site/financial/templates/financial/profile.html @@ -1,18 +1,19 @@ - - - +{% extends "base.html" %} {% load static %} - - - Profile - - - +{% block title %}Profile - AI ML Operations{% endblock %} -
- - -

Profile

- {{ form }} - \ No newline at end of file +{% block content %} +
+
+

Profile

+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/reports.html b/company_site/financial/templates/financial/reports.html new file mode 100644 index 0000000..1213f98 --- /dev/null +++ b/company_site/financial/templates/financial/reports.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Client Reports - AI ML Operations{% endblock %} + +{% block content %} +
+
+
+

Client Reports

+ Back to + Dashboard +
+ +
+ {% if contracts %} + + + + + + + + + + + {% for c in contracts %} + + + + + + + {% endfor %} + +
Contract NameTotal Budget (Hours)Total Logged (Hours)Remaining Budget (Hours)
{{ c.name }}{{ c.budget_hours }}{{ c.total_logged }}{{ + c.remaining_budget }}
+ {% else %} +

No data available to report.

+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/time_logs.html b/company_site/financial/templates/financial/time_logs.html new file mode 100644 index 0000000..0386475 --- /dev/null +++ b/company_site/financial/templates/financial/time_logs.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Time Logs - AI ML Operations{% endblock %} + +{% block content %} +
+
+
+

All Time Logs

+ +
+ +
+ + + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
EmployeeContractDateStart TimeEnd TimeDuration (hrs)Actions
{{ log.timeCard.employee }}{{ log.contract }}{{ log.date }}{{ log.start_time|default_if_none:"" }}{{ log.end_time|default_if_none:"" }}{{ log.hour }} + Edit +
+ {% csrf_token %} + +
+
No time + logs found.
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/templates/financial/timekeeping.html b/company_site/financial/templates/financial/timekeeping.html new file mode 100644 index 0000000..08159a8 --- /dev/null +++ b/company_site/financial/templates/financial/timekeeping.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Timekeeping - AI ML Operations{% endblock %} + +{% block content %} +
+
+ + +

Log Time

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/company_site/financial/urls.py b/company_site/financial/urls.py index 069db3f..c09a196 100644 --- a/company_site/financial/urls.py +++ b/company_site/financial/urls.py @@ -5,13 +5,18 @@ from . import views urlpatterns = [ path("", views.index, name="financial_index"), path("timekeeping", views.timekeeping, name="Timekeeping"), + path("time_logs", views.time_logs, name="time_logs"), + path("time_logs//edit", views.edit_time_log, name="edit_time_log"), + path("time_logs//delete", views.delete_time_log, name="delete_time_log"), path("timeapproval", views.timeapproval, name="Timeapproval"), path("contracts", views.contracts, name="contracts"), path("/contract_detail", views.contract_detail, name="contract_detail"), path("new_contract", views.new_contract, name="new_contract"), + path("new_employee", views.new_employee, name="new_employee"), path("new_charge_number", views.new_charge_number, name="new_charge_number"), path("/update_charge_number", views.update_charge_number, name="update_charge_number"), #path("contracts//", views.contract_detail, name="contract"), path("procurements", views.procurement, name="procurements"), path("profile", views.profile, name="profile"), + path("client_reports", views.client_reports, name="client_reports"), ] \ No newline at end of file diff --git a/company_site/financial/views.py b/company_site/financial/views.py index edc7631..69ae737 100644 --- a/company_site/financial/views.py +++ b/company_site/financial/views.py @@ -1,79 +1,210 @@ from django.shortcuts import render, redirect -from .forms import EmployeeForm, ContractForm, ChargeNumberForm -from .models import Contract -# Create your views here. +from django.contrib.auth.decorators import user_passes_test +from .forms import EmployeeForm, ContractForm, ChargeNumberForm, TimeLogForm, NewEmployeeForm +from .models import Contract, TimeCard, TimeCardCell, Employee +from django.utils import timezone +from django.db.models import Sum +from datetime import timedelta +import json -# PAGES TO CREATE -# dashboard -# log in -# log out -# password reset -# employee timecard -# time card approval -# contract -# charge number -# user management (?) +def is_admin(user): + return user.is_active and user.is_superuser +@user_passes_test(is_admin) def index(request): - permissions = [] - context = { - 'is_procurment_officer':True, - 'is_worker':True, - 'is_manager':True, - 'is_finance': True - - } - return render(request, "financial/index.html", context) - -def contracts(request): contracts = Contract.objects.all() - return render(request, 'financial/contracts.html', {'contracts':contracts}) + for c in contracts: + total = TimeCardCell.objects.filter(contract=c).aggregate(Sum('hour'))['hour__sum'] + c.total_logged = total if total else 0.0 + employees = Employee.objects.all() + employee_data = [] + for e in employees: + contract_hours = [] + for c in contracts: + total_e_c = TimeCardCell.objects.filter(contract=c, timeCard__employee=e).aggregate(Sum('hour'))['hour__sum'] + contract_hours.append({'contract': c, 'hours': total_e_c if total_e_c else 0.0}) + employee_data.append({'employee': e, 'contract_hours': contract_hours}) + + return render(request, "financial/index.html", { + 'contracts': contracts, + 'employee_data': employee_data + }) + +@user_passes_test(is_admin) +def new_employee(request): + if request.method == "POST": + form = NewEmployeeForm(request.POST) + if form.is_valid(): + form.save() + return redirect('financial_index') + else: + form = NewEmployeeForm() + return render(request, 'financial/new_employee.html', {"form": form}) + +@user_passes_test(is_admin) +def contracts(request): + contracts_list = Contract.objects.all() + today = timezone.now().date() + + chart_data_list = [] + + for c in contracts_list: + cells = TimeCardCell.objects.filter(contract=c).select_related('timeCard__employee').order_by('date') + + total_hours = sum((cell.hour or 0.0) for cell in cells) + total_money = sum((cell.hour or 0.0) * cell.timeCard.employee.hourly_salary for cell in cells) + + c.total_hours_spent = total_hours + c.total_money_spent = total_money + c.remaining_hours = max(0, c.budget_hours - total_hours) + + budget_amt = c.funded_amount if c.funded_amount > 0 else c.proposed_amount + c.remaining_money = max(0, budget_amt - total_money) + + proj_end = None + + if cells.exists(): + first_date = cells.first().date or c.baseline_start or today + last_date = cells.last().date or today + days_elapsed = (last_date - first_date).days + if days_elapsed <= 0: + days_elapsed = 1 + + daily_hour_burn = total_hours / days_elapsed + daily_money_burn = total_money / days_elapsed + + days_out_hours = (c.remaining_hours / daily_hour_burn) if daily_hour_burn > 0 else 9999 + days_out_money = (c.remaining_money / daily_money_burn) if daily_money_burn > 0 else 9999 + + days_out = max(days_out_hours, days_out_money) + + if days_out < 9999: + proj_end = last_date + timedelta(days=int(days_out)) + + c.projected_end_date = proj_end if proj_end else "N/A" + + start_dt = str(c.baseline_start or today) + end_dt = str(proj_end or (today + timedelta(days=30))) + + chart_data_list.append({ + 'name': c.name, + 'start_date': start_dt, + 'today': str(today), + 'projected_end': end_dt, + 'budget': float(budget_amt), + 'spent': float(total_money), + 'remaining': float(c.remaining_money) + }) + + return render(request, 'financial/contracts.html', { + 'contracts': contracts_list, + 'chart_data_json': json.dumps(chart_data_list) + }) + +@user_passes_test(is_admin) def contract_detail(request, contract_slug): - contract = Contract.objects.filter(slug=contract_slug) + contract = Contract.objects.filter(slug=contract_slug).first() if request.method == 'POST': - form = ContractForm(request.POST, instance=contract[0]) + form = ContractForm(request.POST, instance=contract) if form.is_valid(): form.save() return redirect('contracts') else: - form = ContractForm(instance = contract[0]) + form = ContractForm(instance=contract) charge_number_form = ChargeNumberForm() - # TODO: handle multiple better but we can assume there is only one - - return render(request, 'financial/contract_detail.html', {'is_new': False, 'form': form, 'charge_number_form':charge_number_form, 'contract': contract[0]}) + return render(request, 'financial/contract_detail.html', {'is_new': False, 'form': form, 'charge_number_form':charge_number_form, 'contract': contract}) +@user_passes_test(is_admin) def new_contract(request): if request.method == "POST": form = ContractForm(request.POST) if form.is_valid(): form.save() return redirect('contracts') - else: - return render(request, 'financial/contract_detail.html', {"form": ContractForm(), 'is_new': True}) - else: - return render(request, 'financial/contract_detail.html', {"form": ContractForm(), 'is_new': True}) + form = ContractForm() + return render(request, 'financial/contract_detail.html', {"form": form, 'is_new': True}) +@user_passes_test(is_admin) +def timekeeping(request): + if request.method == "POST": + form = TimeLogForm(request.POST) + if form.is_valid(): + employee = Employee.objects.filter(user=request.user).first() + if not employee: + employee = Employee.objects.first() + if not employee: + return render(request, 'financial/timekeeping.html', {"form": form, "error": "No Employee exists. Cannot auto-create TimeCard. Please create an Employee first."}) + + time_card, _ = TimeCard.objects.get_or_create(employee=employee, startDate=timezone.now().date(), endDate=timezone.now().date()) + cell = form.save(commit=False) + cell.timeCard = time_card + cell.save() + return redirect('financial_index') + else: + form = TimeLogForm() + return render(request, 'financial/timekeeping.html', {'form': form}) + +@user_passes_test(is_admin) +def time_logs(request): + logs = TimeCardCell.objects.all().order_by('-date', '-created') + return render(request, 'financial/time_logs.html', {'logs': logs}) + +@user_passes_test(is_admin) +def edit_time_log(request, log_id): + log_entry = TimeCardCell.objects.filter(id=log_id).first() + if not log_entry: + return redirect('time_logs') + + if request.method == "POST": + form = TimeLogForm(request.POST, instance=log_entry) + if form.is_valid(): + form.save() + return redirect('time_logs') + else: + form = TimeLogForm(instance=log_entry) + + return render(request, 'financial/edit_time_log.html', {'form': form, 'log': log_entry}) + +@user_passes_test(is_admin) +def delete_time_log(request, log_id): + if request.method == "POST": + log_entry = TimeCardCell.objects.filter(id=log_id).first() + if log_entry: + log_entry.delete() + return redirect('time_logs') + +@user_passes_test(is_admin) +def client_reports(request): + contracts = Contract.objects.all() + for c in contracts: + total = TimeCardCell.objects.filter(contract=c).aggregate(Sum('hour'))['hour__sum'] + c.total_logged = total if total else 0.0 + c.remaining_budget = c.budget_hours - c.total_logged + return render(request, 'financial/reports.html', {'contracts': contracts}) + +@user_passes_test(is_admin) def update_charge_number(request, charge_number_slug): return render(request, 'financial/not_created.html', {}) +@user_passes_test(is_admin) def new_charge_number(request, charge_number_slug): return render(request, 'financial/not_created.html', {}) -def timekeeping(request): - return render(request, 'financial/not_created.html', {}) - +@user_passes_test(is_admin) def timeapproval(request): return render(request, 'financial/not_created.html', {}) +@user_passes_test(is_admin) def chargenumber(request): return render(request, 'financial/not_created.html', {}) +@user_passes_test(is_admin) def procurement(request): return render(request, 'financial/procurement.html', {}) +@user_passes_test(is_admin) def profile(request): form = EmployeeForm() - return render(request, 'financial/profile.html', {'form': form}) -# def contract_detail(request, contract_id): \ No newline at end of file + return render(request, 'financial/profile.html', {'form': form}) \ No newline at end of file diff --git a/company_site/public/static/public/css/style.css b/company_site/public/static/public/css/style.css index a170904..764b028 100644 --- a/company_site/public/static/public/css/style.css +++ b/company_site/public/static/public/css/style.css @@ -458,4 +458,69 @@ nav { .hero-image { transform: none; } +} + +/* Table Styles */ +.table { + width: 100%; + margin-bottom: 1rem; + color: var(--text-color); + border-collapse: collapse; +} + +.table th, +.table td { + padding: 1rem; + vertical-align: top; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid rgba(255, 255, 255, 0.1); + color: var(--primary-color); + text-align: left; + font-weight: 600; +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Django Form Styling defaults */ +input[type="text"], +input[type="number"], +input[type="email"], +input[type="password"], +input[type="date"], +input[type="time"], +input[type="datetime-local"], +select, +textarea { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-color); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: white; + font-family: var(--font-main); + font-size: 1rem; + margin-bottom: 1rem; + transition: border-color var(--transition-speed); +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); +} + +.helptext { + color: var(--text-muted); + font-size: 0.85rem; + display: block; + margin-bottom: 1rem; + margin-top: -0.5rem; } \ No newline at end of file