Add customer-specific filtering and refactor order invoice logic

Introduced customer attribute-based filtering for individual invoicing and enhanced collected order invoice processing with proper associations to customers and orders. Added new endpoints, fields, and utility methods to streamline data retrieval, ensure consistency, and support new use cases like 'Ready to Invoice'. Includes minor fixes, validations, and optimizations throughout the affected modules.
This commit is contained in:
Jepp9350
2025-03-13 14:35:49 +01:00
parent f045611794
commit 32f0a1548f
10 changed files with 179 additions and 34 deletions
+3
View File
@@ -61,6 +61,9 @@ $cron_tasks = [
function checkUnfulfilledBookings(): void
{
// This is deactivated for now, as it is not wanted.
// I'm saving this for later, as it is a good idea to have this in place.
return;
$bookings_o = new bookings_o();
$bookings_o->checkUnfulfilledBookings();
}
@@ -25,6 +25,7 @@ class collected_order_invoices_o extends db
public array $processors = [
1 => 'E-conomic',
2 => 'Stripe',
3 => 'Other, without tracking',
];
public function structure(): void
@@ -642,5 +643,12 @@ class collected_order_invoices_o extends db
return $this;
}
public function listCustomersWithIndividualOrderInvoicing(): array
{
// Get all the customers with the "invoiceAllOrdersIndividually" attribute
$users = new users_o();
return $users->getCustomerNumbersWithAttributes([
'invoiceAllOrdersIndividually'
]);
}
}
@@ -78,15 +78,24 @@ class department_daily_reports_o extends db
int $water_usage,
int $water_usage_morning,
string $notes,
int $filled_by
int $filled_by,
string $created_at = null
): department_daily_reports_o
{
// Validate the created_at date, if provided
if ($created_at !== null) {
$dateTime = \DateTime::createFromFormat('Y-m-d', $created_at);
if (!$dateTime || $dateTime->format('Y-m-d') !== $created_at) {
throw new Exception('Invalid date format for created_at. Expected format: YYYY-MM-DD');
}
}
$tmp_id = $this->add_object([
'department_id' => (int)$department_id,
'water_usage' => (int)$water_usage,
'water_usage_morning' => (int)$water_usage_morning,
'notes' => (string)$notes,
'filled_by' => (int)$filled_by,
'created_at' => $created_at ? $created_at : date('Y-m-d H:i:s'),
]);
$this->id = $tmp_id;
$this->getObjectProperties();
+21 -21
View File
@@ -142,8 +142,8 @@ class orders_o extends db
{
return [
'id' => $this->id,
'customer_id' => $this->customer_id->value(),
'cashier_id' => $this->cashier_id->value(),
'customer_id' => (int)$this->customer_id->value(),
'cashier_id' => (int)$this->cashier_id->value(),
'reference' => $this->reference->value(),
'notes' => $this->notes->value(),
'department_id' => (int)$this->department_id->value(),
@@ -152,11 +152,30 @@ class orders_o extends db
'reg_3' => $this->reg_3->value(),
'created_at' => $this->created_at->value(),
'deleted_at' => $this->deleted_at->value(),
'total_net_amount' => $this->getNetAmount(),
'invoice_collection_id' => (int)$this->invoice_collection_id->value(),
'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null,
];
}
public function getNetAmount(): float
{
self::requireSelected();
$net = 0;
// Get the order items object
$order_items = new order_items_o();
// Get the price, quantity of the order items
$items = $order_items->getAllItemsAsArray($this->id);
// Loop through the items and get the net amount
foreach ( $items as $item ) {
$tmp_price = (int)$item['price'];
$tmp_quantity = (int)$item['quantity'];
// Add the price to the net amount
$net += $tmp_price * $tmp_quantity;
}
return $net;
}
public function exists(): bool
{
// Check if the id is greater than 0, and that the deleted_at property is null
@@ -350,7 +369,6 @@ class orders_o extends db
self::objectChanged();
}
public function objectChanged(): void
{
// Since the orders object is not cached, there is no need to invalidate the cache
@@ -446,22 +464,4 @@ class orders_o extends db
$result = $db->query($sql);
return $db->fetch_all($result);
}
public function getNetAmount(): float
{
self::requireSelected();
$net = 0;
// Get the order items object
$order_items = new order_items_o();
// Get the price, quantity of the order items
$items = $order_items->getAllItemsAsArray($this->id);
// Loop through the items and get the net amount
foreach ( $items as $item ) {
$tmp_price = (int)$item['price'];
$tmp_quantity = (int)$item['quantity'];
// Add the price to the net amount
$net += $tmp_price * $tmp_quantity;
}
return $net;
}
}
+41
View File
@@ -924,4 +924,45 @@ class users_o extends db
$invoice_collection = new collected_order_invoices_o();
return $invoice_collection->getLatestOpenInvoiceCollection($this->customer_number->value());
}
/**
* Get all customers with the given attributes
* @notation Retrieve all customers with ALL the given attributes
* @param array $attributes The attributes to search for (e.g. ['attribute1', 'attribute2'])
* @return array The e-conomic customer numbers with the given attributes (e.g. [123456, 654321])
*/
public function getCustomerNumbersWithAttributes(array $attributes): array
{
global $db;
// Create an array to store the customer numbers
$user_ids = [];
$customer_numbers = [];
// Loop through the attributes
foreach ( $attributes as $attribute ) {
// Get the customer numbers with the attribute
$sql = "SELECT user_id FROM customer_attributes WHERE attribute = '$attribute'";
$result = $db->query($sql);
// Loop through the results
while ($row = $result->fetch_assoc()) {
// Add the customer number to the array
$user_ids[] = (int)$row['user_id'];
}
}
// Remove duplicates from the array
$user_ids = array_unique($user_ids);
// Get the customer numbers from the user IDs
foreach ( $user_ids as $user_id ) {
// Get the customer number from the user ID
$sql = "SELECT customer_number FROM $this->table WHERE id = $user_id";
$result = $db->query($sql);
// Loop through the results
while ($row = $result->fetch_assoc()) {
// Add the customer number to the array
$customer_numbers[] = (int)$row['customer_number'];
}
}
// Remove duplicates from the array
// Return the customer numbers
return array_unique($customer_numbers);
}
}
@@ -201,6 +201,7 @@ class bookingsRoute
// Require the user to be logged in
global /** @var response $response */
$response;
// Check if the user has access to the department
$this->requirePermission('download_own_wash_certificate');
// Get the user object
$user = (new authentication())->get_user();
@@ -118,7 +118,15 @@ class departmentDailyReportsRoute
(string)self::getParameter('date')
);
} catch (\Exception $e) {
$response->error('No department daily report found', 404);
// Create a new department daily report if it does not exist
$department_daily_report_latest = $department_daily_reports_o->add(
(int)self::getParameter('id'),
0,
0,
'',
$user->id,
(string)self::getParameter('date')
);
}
$response->success(
[
@@ -35,22 +35,36 @@ class orderInvoicesRoute
$collected_order_invoices->requireSelected();
$response->success($collected_order_invoices->asArray());
}
// Define the users
$users = new users_o();
// Define the collected order invoices
$tmp_collected_order_invoices = new collected_order_invoices_o();
// Return the list of collected order invoices
$response->success($collected_order_invoices->listObjectsWithPaginationIfSet(
function ($collected_order_invoice) {
function ($collected_order_invoice) use ($tmp_collected_order_invoices, $users) {
// Select the orders for each collected order invoice
$tmp_collected_order_invoices->select((int)$collected_order_invoice['id']);
return [
'id' => (int)$collected_order_invoice['id'],
'name' => (string)$collected_order_invoice['name'],
'notes' => (string)$collected_order_invoice['notes'],
'customer_number' => (int)$collected_order_invoice['customer_number'],
'customer_name' => (string)$users->getCustomerName((int)$collected_order_invoice['customer_number']),
'processor' => $collected_order_invoice['processor'] ? (int)$collected_order_invoice['processor'] : null,
'external_id' => (string)$collected_order_invoice['external_id'],
'closed_at' => $collected_order_invoice['closed_at'] ? (string)$collected_order_invoice['closed_at'] : null,
'updated_at' => (string)$collected_order_invoice['updated_at'],
'created_at' => (string)$collected_order_invoice['created_at'],
'orders' => ((new collected_order_invoices_o())->select((int)$collected_order_invoice['id']))->getOrders(true),
'orders' => $tmp_collected_order_invoices->getOrders(true),
'total_net_amount' => (float)$tmp_collected_order_invoices->getTotalAmount()
//'debug' => $collected_order_invoice,
];
}
},
null,
[
// This adds the customer table to the query
//'users' => 'users.customer_number = collected_order_invoices.customer_number',
]
));
} else {
(new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES', 'User tried to access the list of collected order invoices without a valid session');
@@ -62,6 +76,55 @@ class orderInvoicesRoute
]
);
/** Collected order invoices > Ready to invoice > GET */
$this->get('/collected-invoices/ready-to-invoice', function () {
global $response;
self::requirePermission('list_collected_invoices');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'LIST_COLLECTED_INVOICES_READY_TO_INVOICE', 'User accessed the list of collected order invoices ready to invoice');
$collected_order_invoices = new collected_order_invoices_o();
// Define the users
$users = new users_o();
// Define the collected order invoices
$tmp_collected_order_invoices = new collected_order_invoices_o();
// Return the list of collected order invoices
$response->success($collected_order_invoices->listObjectsWithPaginationIfSet(
function ($collected_order_invoice) use ($tmp_collected_order_invoices, $users) {
// Select the orders for each collected order invoice
$tmp_collected_order_invoices->select((int)$collected_order_invoice['id']);
return [
'id' => (int)$collected_order_invoice['id'],
'name' => (string)$collected_order_invoice['name'],
'notes' => (string)$collected_order_invoice['notes'],
'customer_number' => (int)$collected_order_invoice['customer_number'],
'customer_name' => (string)$users->getCustomerName((int)$collected_order_invoice['customer_number']),
'processor' => $collected_order_invoice['processor'] ? (int)$collected_order_invoice['processor'] : null,
'external_id' => (string)$collected_order_invoice['external_id'],
'closed_at' => $collected_order_invoice['closed_at'] ? (string)$collected_order_invoice['closed_at'] : null,
'updated_at' => (string)$collected_order_invoice['updated_at'],
'created_at' => (string)$collected_order_invoice['created_at'],
'orders' => $tmp_collected_order_invoices->getOrders(true),
'total_net_amount' => (float)$tmp_collected_order_invoices->getTotalAmount(),
];
},
$collected_order_invoices->forceRestrictFilters(
[
// This makes sure that the user can only see orders from the departments they explicitly have access to
'customer_number' => $collected_order_invoices->listCustomersWithIndividualOrderInvoicing(),
]
)
));
} else {
(new logs_o())->add('orderInvoices', 'global', 0, 0, 'LIST_COLLECTED_INVOICES_READY_TO_INVOICE', 'User tried to access the list of collected order invoices ready to invoice without a valid session');
$response->error('Invalid session', 400);
}
},
[
'list_collected_invoices' => 'List ALL collected order invoices. This is a superuser-only route.'
]
);
/** Collected order invoices > POST */
$this->post('/collected-invoices', function () {
global $response;
@@ -38,6 +38,8 @@ class ordersRoute
function ($order) {
// Add the invoice status to the order
$order['economic_invoice_module'] = (new economic_module_orders())->getByOrderId($order['id'])->asArray();
// Add the total amount to the order
$order['total_net_amount'] = (new orders_o())->select($order['id'])->getNetAmount();
// Add the stripe status to the order
$stripe_module_orders = (new stripe_module_orders_o())->select($order['id']);
if ($stripe_module_orders->exists()) {
+17 -7
View File
@@ -136,10 +136,10 @@ trait db_object_t
* @return array The list of objects in the table
* @throws Exception
*/
public function listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null): array
public function listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): array
{
// Link to the listObjectsWithPaginationIfSet function.
return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters);
return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters, $join);
}
/**
@@ -150,7 +150,7 @@ trait db_object_t
* @return array
* @throws Exception If the user does not have permission to list the objects
*/
public function db_object_t__listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null): array
public function db_object_t__listObjectsWithPaginationIfSet($parseFunction = null, $forcedFilters = null, array $join = []): array
{
global $response;
$page = ((int)$response->getRequestParameter('page')) ?? null; // Get the page number
@@ -200,10 +200,10 @@ trait db_object_t
}
// List the objects with pagination
if ($page && $limit) {
return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction);
return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction, $join);
}
// If the page and limit are not set, list all objects
return $this->listObjectsWithPagination(1, 1000, $search, $filters, $order, $parseFunction);
return $this->listObjectsWithPagination(1, 1000, $search, $filters, $order, $parseFunction, $join);
}
/**
@@ -213,7 +213,7 @@ trait db_object_t
* @return array The list of objects in the table
* @throws Exception
*/
public function listObjectsWithPagination(int $page, int $limit, ?string $search = null, ?array $filters = null, ?array $order = null, $parseFunction = null): array
public function listObjectsWithPagination(int $page, int $limit, ?string $search = null, ?array $filters = null, ?array $order = null, $parseFunction = null, array $join = []): array
{
global /** @var response $response */
/** @var db $db */
@@ -237,6 +237,7 @@ trait db_object_t
$whereClauses = [];
$params = [];
$joinClauses = [];
// Add where clauses from the object, if set. This is done to allow for custom where clauses in routes, while still allowing for pagination
if (!empty($this->whereClauses)) {
@@ -309,8 +310,17 @@ trait db_object_t
$params[] = $limit;
$params[] = $offset;
// Join clause with table prefix, so it doesn't conflict with the other columns // This is done to allow for custom join clauses in routes, while still allowing for pagination
$joinClauses = '';
if (!empty($join)) {
// Build the join clauses
foreach ( $join as $table => $on ) {
$joinClauses .= " LEFT JOIN `$table` ON $on";
}
}
// Final query
$sql = "SELECT * FROM {$this->table} $whereQuery $orderQuery $limitQuery";
$sql = "SELECT * FROM {$this->table} $joinClauses $whereQuery $orderQuery $limitQuery";
// Prepare and bind
$stmt = $mysqli->prepare($sql);