Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,102 @@
- Drop image onto dropzone or click dropzone to open file picker.
- Selected file name shown immediately.

### Step 40 - Workforce Attendance + Full Payroll Deductions
- Added `Attendance` model for daily employee timekeeping with:
- status (`present`, `absent`, `leave`)
- schedule/clock fields
- auto-tracked `late_minutes` and `undertime_minutes`
- per-employee, per-date uniqueness constraint
- Extended `Payroll` model with full deduction breakdown:
- `absent_days`, `late_minutes`, `undertime_minutes`
- `late_deduction`, `undertime_deduction`, `absent_deduction`
- `other_deductions`, `additions`, computed `deductions`, and `net_pay`
- Implemented payroll computation service (`apps/workforce/services.py`) to calculate deductions from attendance records by payroll period.
- Added full Attendance web CRUD flow:
- Backend: forms, views, urls
- Templates: list/create-edit/delete confirmation pages
- Updated workforce dashboard and payroll list UI to show attendance metrics and detailed payroll deduction outputs.
- Registered Attendance in Django admin and expanded Payroll admin columns.
- Generated and applied migration:
- `workforce.0003_attendance_payroll_absent_days_and_more`
- Verified framework integrity with `manage.py check` (no issues).

### Step 41 - Full Module Test Suite
- Replaced placeholder module tests with real coverage for:
- `authentication`, `clients`, `sales`, `inventory`, `finance`, `audit`, `workforce`
- Added API status endpoint tests across all modules under `/api/v1/.../status/`.
- Added core domain tests per module including:
- model string representations and key model constraints
- service-layer business rules (`SalesOrderService`, `JournalService`, `PayrollComputationService`)
- workforce attendance lateness/undertime calculation behavior
- Added new test file:
- `apps/workforce/tests.py`
- Executed full test run successfully:
- `manage.py test` → **17 tests passed**.

### Step 42 - HR-First Employee Profile Expansion
- Expanded `workforce.Employee` to support richer HR management data:
- `job_title`
- `employment_status` (`Active`, `Probation`, `Resigned`, `Terminated`)
- `manager` (self-referential reporting line)
- `emergency_contact_name`
- `emergency_contact_phone`
- Updated workforce Employee CRUD form to capture all new HR profile fields.
- Updated workforce Employee list page to show job title, status, manager, and emergency contact details.
- Updated Django admin Employee configuration to filter/search by HR profile fields.
- Generated and applied migration:
- `workforce.0004_employee_emergency_contact_name_and_more`
- Validation completed successfully:
- `manage.py check` (no issues)
- `manage.py test apps.workforce` (3 passed)
- `manage.py test` (17 passed)

### Step 43 - Employee Tenure Tracking (Years/Months/Days)
- Added automatic tenure computation on `workforce.Employee` from `hire_date`:
- `tenure_breakdown` (`years`, `months`, `days`)
- `tenure_display` formatted as `Xy Xm Xd`
- Updated workforce employee list table to show a new **Tenure** column beside hire date.
- Added workforce test coverage for tenure formatting:
- `test_employee_tenure_display_for_today_hire_date`
- Validation completed:
- `manage.py check` (no issues)
- `manage.py test apps.workforce` (4 passed)

### Step 44 - Sidebar Collapse + Wide Table Readability
- Added a collapsible left sidebar in shared layout (`templates/base.html`) with:
- topbar toggle button (`Collapse`/`Expand`)
- persisted state using `localStorage` (`erp-sidebar-collapsed`)
- compact collapsed navigation labels (short module codes)
- Improved wide-table usability globally by wrapping all tables in horizontal scroll containers.
- Updated table cell behavior to avoid compressed columns by keeping cell content on one line and allowing horizontal scroll.
- Validation completed:
- `manage.py check` (no issues)
- `manage.py test apps.workforce` (4 passed)

### Step 45 - Employee Schedule Settings (Mon-Sun, Variable Times)
- Added new `EmployeeSchedule` model in workforce for per-employee weekly schedule settings:
- weekday (`Monday` to `Sunday`)
- `scheduled_start`, `scheduled_end`
- `is_day_off`
- Added validation rules for schedules:
- start/end required for working days
- end time must be later than start time
- one schedule per employee per weekday (unique constraint)
- Added full Schedule CRUD flow in workforce:
- backend forms/views/routes/admin
- templates: `schedule_list`, `schedule_form`, `schedule_confirm_delete`
- dashboard quick action and schedule KPI/recent table
- Integrated schedules with attendance processing:
- attendance creation now auto-applies configured schedule for the selected employee/date
- day-off schedule auto-sets attendance status to `leave`
- late/undertime calculations now use configured schedule times
- Generated and applied migration:
- `workforce.0005_alter_attendance_scheduled_end_and_more`
- Added/updated workforce tests for schedule validation and attendance schedule auto-application.
- Validation completed:
- `manage.py check` (no issues)
- `manage.py test apps.workforce` (6 passed)

### Next
- Apply migrations against the target PostgreSQL instance.
- Continue CRUD implementation for Audit views and Sales/Finance line-item workflows.
28 changes: 27 additions & 1 deletion apps/audit/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
from django.contrib.auth import get_user_model
from django.test import TestCase

# Create your tests here.
from .models import AuditEvent


class AuditModuleTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='audit-user',
email='audit-user@example.com',
password='password123',
)

def test_audit_status_endpoint_authenticated(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/audit/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'audit', 'status': 'ok'})

def test_audit_event_string_value(self):
event = AuditEvent.objects.create(
module='workforce',
action='create_payroll',
entity_type='Payroll',
entity_id='1',
actor=self.user,
)
self.assertIn('workforce:create_payroll:Payroll:1', str(event))
30 changes: 29 additions & 1 deletion apps/authentication/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
from django.contrib.auth import get_user_model
from django.test import TestCase

# Create your tests here.
from .models import Company, UserProfile


class AuthenticationModuleTests(TestCase):
def setUp(self):
self.user_model = get_user_model()
self.user = self.user_model.objects.create_user(
username='auth-user',
email='auth-user@example.com',
password='password123',
)

def test_auth_status_endpoint_requires_authenticated_user(self):
response = self.client.get('/api/v1/auth/status/')
self.assertIn(response.status_code, [401, 403])

def test_auth_status_endpoint_returns_ok_for_authenticated_user(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/auth/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'authentication', 'status': 'ok'})

def test_company_and_user_profile_string_values(self):
company = Company.objects.create(name='Acme Corporation', code='ACME')
profile = UserProfile.objects.create(user=self.user, company=company, role=UserProfile.Role.OWNER)

self.assertIn('ACME', str(company))
self.assertIn(self.user.username, str(profile))
25 changes: 24 additions & 1 deletion apps/clients/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
from django.contrib.auth import get_user_model
from django.test import TestCase

# Create your tests here.
from .models import Client, ClientContact


class ClientsModuleTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='clients-user',
email='clients-user@example.com',
password='password123',
)

def test_clients_status_endpoint_authenticated(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/clients/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'clients', 'status': 'ok'})

def test_client_and_contact_string_values(self):
client = Client.objects.create(code='C001', name='Globex LLC')
contact = ClientContact.objects.create(client=client, full_name='Jane Contact', is_primary=True)

self.assertEqual(str(client), 'C001 - Globex LLC')
self.assertIn('Jane Contact', str(contact))
56 changes: 55 additions & 1 deletion apps/finance/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone

from apps.audit.models import AuditEvent

from .models import Account, JournalEntry, JournalLine
from .services import JournalService


class FinanceModuleTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='finance-user',
email='finance-user@example.com',
password='password123',
)
self.cash = Account.objects.create(code='1000', name='Cash', account_type=Account.AccountType.ASSET)
self.revenue = Account.objects.create(code='4000', name='Revenue', account_type=Account.AccountType.REVENUE)

def test_finance_status_endpoint_authenticated(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/finance/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'finance', 'status': 'ok'})

def test_journal_service_rejects_unbalanced_lines(self):
with self.assertRaises(ValidationError):
JournalService.post_entry(
entry_number='JE-UNBAL-1',
entry_date=timezone.localdate(),
description='Unbalanced test',
lines=[
{'account': self.cash, 'debit': Decimal('100.00'), 'credit': Decimal('0')},
{'account': self.revenue, 'debit': Decimal('0'), 'credit': Decimal('90.00')},
],
posted_by=self.user,
)

def test_journal_service_posts_balanced_entry_and_logs_audit(self):
entry = JournalService.post_entry(
entry_number='JE-0001',
entry_date=timezone.localdate(),
description='Balanced test',
lines=[
{'account': self.cash, 'debit': Decimal('100.00'), 'credit': Decimal('0')},
{'account': self.revenue, 'debit': Decimal('0'), 'credit': Decimal('100.00')},
],
posted_by=self.user,
)

# Create your tests here.
self.assertEqual(JournalEntry.objects.count(), 1)
self.assertEqual(JournalLine.objects.filter(journal_entry=entry).count(), 2)
self.assertTrue(AuditEvent.objects.filter(module='finance', action='post_journal_entry').exists())
38 changes: 37 additions & 1 deletion apps/inventory/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase

# Create your tests here.
from .models import Category, Product, StockLedger, Warehouse


class InventoryModuleTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='inventory-user',
email='inventory-user@example.com',
password='password123',
)

def test_inventory_status_endpoint_authenticated(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/inventory/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'inventory', 'status': 'ok'})

def test_stock_ledger_quantity_must_be_positive(self):
category = Category.objects.create(code='CAT-01', name='Category 01')
product = Product.objects.create(sku='INV-001', name='Inventory Item', category=category)
warehouse = Warehouse.objects.create(code='WH-01', name='Main Warehouse')

ledger = StockLedger(
product=product,
warehouse=warehouse,
movement_type=StockLedger.MovementType.INBOUND,
quantity=Decimal('0'),
reference='TEST',
performed_by=self.user,
)

with self.assertRaises(ValidationError):
ledger.full_clean()
45 changes: 44 additions & 1 deletion apps/sales/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone

from apps.audit.models import AuditEvent
from apps.clients.models import Client
from apps.inventory.models import Product

from .models import SalesOrder, SalesOrderLine
from .services import SalesOrderService


class SalesModuleTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username='sales-user',
email='sales-user@example.com',
password='password123',
)
self.client_obj = Client.objects.create(code='CL-SALES', name='Sales Client')
self.product = Product.objects.create(sku='SKU-001', name='Item 1', selling_price=Decimal('15.00'))

def test_sales_status_endpoint_authenticated(self):
self.client.force_login(self.user)
response = self.client.get('/api/v1/sales/status/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'module': 'sales', 'status': 'ok'})

def test_sales_order_service_creates_lines_total_and_audit_event(self):
order = SalesOrderService.create_order(
client=self.client_obj,
order_number='SO-1001',
order_date=timezone.localdate(),
lines=[
{'product': self.product, 'quantity': Decimal('2'), 'unit_price': Decimal('10.00')},
{'product': self.product, 'quantity': Decimal('1'), 'unit_price': Decimal('5.50')},
],
created_by=self.user,
)

# Create your tests here.
self.assertEqual(order.total_amount, Decimal('25.50'))
self.assertEqual(SalesOrder.objects.count(), 1)
self.assertEqual(SalesOrderLine.objects.count(), 2)
self.assertTrue(AuditEvent.objects.filter(module='sales', action='create_order').exists())
Loading