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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user