Clients Expense Report Controller Documentation
File: /controllers/clientsexpensereport.php
Purpose: Generates comprehensive customer expense and transaction analysis reports using RedBeanPHP for database operations
Last Updated: December 20, 2024
Total Functions: 2
Lines of Code: ~119
---
๐ Overview
The Clients Expense Report Controller is a modern reporting module that provides detailed customer expense and transaction analysis. Unlike other controllers in the system that use traditional DAO classes, this controller leverages RedBeanPHP ORM for direct database operations, making it more agile for complex reporting queries. The system features:
- โข Comprehensive customer transaction analysis
- โข Multi-source expense tracking (sales, returns, expenses, debt changes)
- โข Detailed vs summary reporting modes
- โข Date range filtering capabilities
- โข Customer-specific drill-down analysis
- โข Product-level detail reporting
- โข RedBeanPHP-based direct SQL operations
Primary Functions
- โ Customer expense summary reporting
- โ Individual customer detailed analysis
- โ Multi-source transaction aggregation
- โ Date range filtering and analysis
- โ Product-level expense breakdown
- โ Sales vs returns vs expenses analysis
- โ Debt change tracking and reporting
Related Controllers
- โข sellbillController.php - Sales operations
- โข returnsellbillController.php - Sales returns
- โข expensesController.php - Expense management
- โข clientController.php - Customer management
- โข clientdebtchange.php - Debt tracking
---
๐๏ธ Database Tables
Primary Tables (Direct RedBeanPHP Access)
| Table Name | Purpose | Key Columns | |
|---|---|---|---|
| **client** | Customer master data | clientid, clientname, clientdebt, conditions | |
| **sellbill** | Sales transactions | sellbillid, sellbillclientid, sellbilltotalpayed, sellbilldate, conditions | |
| **returnsellbill** | Return transactions | returnsellbillid, returnsellbillclientid, returnsellbillaftertotalbill, returnsellbilldate, conditions | |
| **expenses** | Customer expenses | expensesid, clientid, expensesValue, expensesdate, conditions | |
| **clientdebtchange** | Debt tracking | clientdebtchangeid, clientid, clientdebtchangeamount, clientdebtchangedate |
| Table Name | Purpose | Key Columns | |
|---|---|---|---|
| **sellbilldetail** | Sales line items | sellbilldetailid, sellbillid, sellbilldetailproductid, sellbilldetailquantity | |
| **returnsellbilldetail** | Return line items | returnsellbilldetailid, returnsellbillid, returnsellbilldetailproductid, returnsellbilldetailquantity | |
| **product** | Product master | productId, productName, productCatId | |
| **productcat** | Product categories | productCatId, productCatName |
๐ Key Functions
1. Default Action - Report Interface Display
Location: Line 6-9
Purpose: Display the client expense report interface
Process Flow:
if (empty($do)) {
$smarty->display("header.html");
$smarty->display("clientsexpensereportview/show.html");
$smarty->display("footer.html");
}
Simple Interface:
- โข Minimal controller logic
- โข Direct template display
- โข Form-based parameter collection
---
2. getdata Action - Comprehensive Expense Analysis Engine
Location: Line 10-116
Purpose: Process expense analysis requests and generate detailed reports
Function Signature:
// Form Parameters
$start_date = filter_input(INPUT_POST, 'start_date');
$end_date = filter_input(INPUT_POST, 'end_date');
$clientid = filter_input(INPUT_POST, 'clientid');
Dual Reporting Modes:
Mode 1: Individual Customer Detailed Analysis
When: $clientid is provided
Purpose: Detailed breakdown for specific customer
Process Flow:
if($clientid) {
// Load customer master data
$client = R::getRow('SELECT * FROM client WHERE clientid = ' . $clientid);
// Load sales bills with date filtering
$sellbill = R::getAll('SELECT * FROM sellbill WHERE sellbill.conditions = 0
AND sellbill.sellbillclientid = ' . $clientid . $sellQuery);
// Process each sales bill with product details
$sellbilldata = [];
foreach($sellbill as $sell) {
$sellbilldetail = R::getAll('SELECT * FROM sellbilldetail
JOIN product ON sellbilldetail.sellbilldetailproductid = product.productId
JOIN productcat ON product.productCatId = productcat.productCatId
WHERE sellbilldetail.sellbillid = ?', [$sell['sellbillid']]);
$sellbilldata[] = [
'sellbillid' => $sell['sellbillid'],
'sellbilldate' => $sell['sellbilldate'],
'sellbilltotalpayed' => $sell['sellbilltotalpayed'],
'sellbilldetail' => $sellbilldetail,
];
}
// Similar processing for returns, expenses, and debt changes
// ...
}
Detailed Data Structure:
// Sales bills with product breakdown
$sellbilldata[] = [
'sellbillid' => $sell['sellbillid'],
'sellbilldate' => $sell['sellbilldate'],
'sellbilltotalpayed' => $sell['sellbilltotalpayed'],
'sellbilldetail' => $sellbilldetail, // Product-level details
];
// Return bills with product breakdown
$returnsellbilldata[] = [
'returnsellbillid' => $return['returnsellbillid'],
'returnsellbilldate' => $return['returnsellbilldate'],
'returnsellbillaftertotalbill' => $return['returnsellbillaftertotalbill'],
'returnsellbilldetail' => $returnsellbilldetail,
];
Mode 2: All Customers Summary Analysis
When: $clientid is not provided
Purpose: High-level summary across all customers
Process Flow:
else {
// Get all customers with debt changes (active customers)
$clients = R::getAll('SELECT * FROM client
JOIN clientdebtchange ON clientdebtchange.clientid = client.clientid
WHERE client.conditions = 0 ' . $clientQuery .
' GROUP BY client.clientid');
$clientsdata = [];
foreach ($clients as $client) {
// Aggregate totals for each customer
$sellbill = R::getcell('SELECT sum(sellbilltotalpayed) FROM sellbill
WHERE sellbill.conditions = 0
AND sellbill.sellbillclientid = ' . $client['clientid'] . $sellQuery);
$returnsellbill = R::getcell('SELECT sum(returnsellbillaftertotalbill) FROM returnsellbill
WHERE returnsellbill.conditions = 0
AND returnsellbill.returnsellbillclientid = ' . $client['clientid'] . $returnQuery);
$expenses = R::getcell('SELECT sum(expensesValue) FROM expenses
WHERE expenses.conditions = 0
AND expenses.clientid = ' . $client['clientid'] . $expensehQuery);
$clientdebtchange = R::getcell('SELECT sum(clientdebtchangeamount) FROM clientdebtchange
WHERE clientdebtchange.clientid = ' . $client['clientid'] . $changeQuery);
$clientsdata[] = [
'clientname' => $client['clientname'],
'clientdebt' => $client['clientdebt'],
'sellbill' => $sellbill,
'returnsellbill' => $returnsellbill,
'expenses' => $expenses,
'clientdebtchange' => $clientdebtchange
];
}
}
---
๐ Workflows
Workflow 1: Individual Customer Analysis
---
Workflow 2: All Customers Summary
---
๐ URL Routes & Actions
| URL Parameter | Function Called | Description | |
|---|---|---|---|
| `do=` (empty) | Default action | Display expense report interface | |
| `do=getdata` | getdata action | Process and display expense analysis |
All Customers Summary:
- โข
start_date- Start date for analysis (YYYY-MM-DD) - โข
end_date- End date for analysis (YYYY-MM-DD) - โข No
clientidparameter
Individual Customer Analysis:
- โข
start_date- Start date for analysis (YYYY-MM-DD) - โข
end_date- End date for analysis (YYYY-MM-DD) - โข
clientid- Specific customer ID for detailed analysis
---
๐งฎ Calculation Methods
Date Query Building
if($start_date != '' && $end_date != '') {
$sellQuery = ' and sellbill.sellbilldate >= "' . $start_date . ' 00-00-00"
and sellbill.sellbilldate <= "' . $end_date . ' 23-59-55"';
$returnQuery = ' and returnsellbill.returnsellbilldate >= "' . $start_date . ' 00-00-00"
and returnsellbill.returnsellbilldate <= "' . $end_date . ' 23-59-55"';
$expensehQuery = ' and expenses.expensesdate >= "' . $start_date . '"
and expenses.expensesdate <= "' . $end_date . '"';
$changeQuery = ' and clientdebtchange.clientdebtchangedate >= "' . $start_date . ' 00-00-00"
and clientdebtchange.clientdebtchangedate <= "' . $end_date . ' 23-59-55"';
}
Aggregate Calculations (Summary Mode)
// Sales total for customer
$sellbill = R::getcell('SELECT sum(sellbilltotalpayed) FROM sellbill
WHERE sellbill.conditions = 0
AND sellbill.sellbillclientid = ' . $client['clientid'] . $sellQuery);
// Returns total for customer
$returnsellbill = R::getcell('SELECT sum(returnsellbillaftertotalbill) FROM returnsellbill
WHERE returnsellbill.conditions = 0
AND returnsellbill.returnsellbillclientid = ' . $client['clientid'] . $returnQuery);
// Expenses total for customer
$expenses = R::getcell('SELECT sum(expensesValue) FROM expenses
WHERE expenses.conditions = 0
AND expenses.clientid = ' . $client['clientid'] . $expensehQuery);
// Debt changes total for customer
$clientdebtchange = R::getcell('SELECT sum(clientdebtchangeamount) FROM clientdebtchange
WHERE clientdebtchange.clientid = ' . $client['clientid'] . $changeQuery);
---
๐ Security & Permissions
Input Sanitization
$start_date = filter_input(INPUT_POST, 'start_date');
$end_date = filter_input(INPUT_POST, 'end_date');
$clientid = filter_input(INPUT_POST, 'clientid');
SQL Injection Prevention
- โข Partial Protection: Uses parameterized queries for some operations
- โข Risk Area: Direct string concatenation in WHERE clauses
- โข RedBeanPHP Protection: Uses parameterized queries where available
Secure Pattern (Used):
$sellbilldetail = R::getAll('SELECT * FROM sellbilldetail
JOIN product ON sellbilldetail.sellbilldetailproductid = product.productId
WHERE sellbilldetail.sellbillid = ?', [$sell['sellbillid']]);
Risky Pattern (Also Used):
$sellbill = R::getAll('SELECT * FROM sellbill WHERE sellbill.conditions = 0
AND sellbill.sellbillclientid = ' . $clientid . $sellQuery);
Access Control
- โข No explicit authentication check in this controller
- โข Relies on session-based access control from application framework
- โข No permission level validation
---
๐ Performance Considerations
Database Optimization Tips
1. Indexes Required:
- sellbill(sellbillclientid, sellbilldate, conditions)
- returnsellbill(returnsellbillclientid, returnsellbilldate, conditions)
- expenses(clientid, expensesdate, conditions)
- clientdebtchange(clientid, clientdebtchangedate)
- client(conditions)
2. Query Optimization:
- Uses direct aggregate functions (SUM) for performance
- Separate queries avoid complex JOINs
- Date filtering applied at database level
3. RedBeanPHP Performance:
- Direct SQL queries for better performance
- Avoids ORM overhead for reporting
- Uses prepared statements where available
Performance Risks
// This GROUP BY query could be slow with large datasets
$clients = R::getAll('SELECT * FROM client
JOIN clientdebtchange ON clientdebtchange.clientid = client.clientid
WHERE client.conditions = 0
GROUP BY client.clientid');
Improvement Suggestion:
-- Use DISTINCT instead of GROUP BY for better performance
SELECT DISTINCT c.clientid, c.clientname, c.clientdebt, c.conditions
FROM client c
INNER JOIN clientdebtchange cdc ON cdc.clientid = c.clientid
WHERE c.conditions = 0;
---
๐ Common Issues & Troubleshooting
1. RedBeanPHP Compatibility Issues
Issue: RedBeanPHP syntax conflicts with other ORM systems
Cause: Mixed ORM usage in single application
Debug:
// Check if RedBeanPHP is properly initialized
if (!class_exists('R')) {
throw new Exception("RedBeanPHP not available");
}
// Test basic connectivity
$test = R::testConnection();
2. Date Format Issues
Issue: Date filtering not working correctly
Cause: Inconsistent date formats
Debug:
echo "Start Date: " . $start_date . "<br>";
echo "End Date: " . $end_date . "<br>";
echo "Sell Query: " . $sellQuery . "<br>";
// Validate date format
if (!DateTime::createFromFormat('Y-m-d', $start_date)) {
throw new Exception("Invalid start date format");
}
3. Missing Product Details
Issue: Product information not showing in detailed report
Cause: JOINs failing due to data inconsistencies
Debug:
-- Check for missing product data
SELECT sbd.sellbillid, sbd.sellbilldetailproductid, p.productName
FROM sellbilldetail sbd
LEFT JOIN product p ON sbd.sellbilldetailproductid = p.productId
WHERE p.productId IS NULL;
-- Check for missing category data
SELECT p.productId, p.productName, p.productCatId, pc.productCatName
FROM product p
LEFT JOIN productcat pc ON p.productCatId = pc.productCatId
WHERE pc.productCatId IS NULL;
4. Performance Issues with Large Datasets
Issue: Reports running slowly or timing out
Cause: Inefficient queries or missing indexes
Solutions:
// Add pagination for large datasets
$limit = 100;
$offset = ($page - 1) * $limit;
$clients = R::getAll('SELECT * FROM client
WHERE conditions = 0
LIMIT ' . $limit . ' OFFSET ' . $offset);
// Add execution time monitoring
$start_time = microtime(true);
// ... query execution ...
$end_time = microtime(true);
echo "Query took: " . ($end_time - $start_time) . " seconds";
---
๐งช Testing Scenarios
Test Case 1: Individual Customer Detailed Analysis
1. Create customer with various transaction types:
- Sales bills with products
- Return bills with products
- Expense records
- Debt change entries
2. Submit form with customer ID and date range
3. Verify all transaction types appear
4. Check product details are correctly joined
5. Validate date filtering works correctly
Test Case 2: All Customers Summary
1. Create multiple customers with different activity levels
2. Submit form without customer ID
3. Verify all active customers appear in summary
4. Check aggregated totals are correct
5. Verify inactive customers (no debt changes) are excluded
Test Case 3: Date Range Filtering
1. Create transactions on different dates
2. Test various date ranges:
- Single day
- Month range
- Year range
- Empty dates (all time)
3. Verify filtering works across all transaction types
4. Test edge cases (start/end of day)
Test Case 4: Data Integrity
1. Test with missing product data
2. Test with missing category data
3. Test with deleted/cancelled transactions
4. Verify error handling for invalid customer IDs
5. Test with empty result sets
---
๐ Related Documentation
- โข CLAUDE.md - PHP 8.2 migration guide
- โข sellbillController.md - Sales operations
- โข expensesController.md - Expense management
- โข clientController.php - Customer management
- โข Database Schema Documentation - Table relationships
---
Documented By: AI Assistant
Review Status: โ Complete
Next Review: When major changes occur