From 60bcbb6e7500ca50087004fed9a97ea9883704a9 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Mon, 7 Apr 2025 07:43:47 +0200 Subject: [PATCH] Enhance invoice handling and filtering mechanisms Refactored invoice draft handling to improve error checks, added support for optional fetch skipping, and enhanced currency management. Expanded filtering capabilities with date range and attribute-based filters. Adjusted Nginx config to increase FastCGI read timeout for long-running processes. --- services/nginx/app/classes/economic.php | 5 +- .../economic_invoices_draft_endpoint.php | 2 +- .../helpers/economic_invoice_draft.php | 10 +- .../objects/collected_order_invoices_o.php | 103 +++++++++++------- .../objects/currency_conversion_rates_o.php | 4 + services/nginx/app/objects/orders_o.php | 2 +- .../app/objects/user_key_value_pairs_o.php | 24 ++++ .../nginx/app/routes/orderInvoicesRoute.php | 4 +- services/nginx/app/routes/ordersRoute.php | 1 + services/nginx/app/traits/db_object_t.php | 75 ++++++++++++- services/nginx/nginx.conf | 1 + 11 files changed, 183 insertions(+), 48 deletions(-) diff --git a/services/nginx/app/classes/economic.php b/services/nginx/app/classes/economic.php index c4759692..c3fe2683 100644 --- a/services/nginx/app/classes/economic.php +++ b/services/nginx/app/classes/economic.php @@ -94,11 +94,12 @@ class economic implements economic_i * Get a invoiceDraft (Helper) from the Economic system * @param int $draft_invoice_number The Economic draft invoice number * @param string $currency The conversion rate to use (default: DKK) + * @param bool $skip_fetch Skip fetching the invoice draft from the Economic system (default: false) * @return economic_invoice_draft */ - public function getInvoiceDraft(int $draft_invoice_number, string $currency = 'DKK'): economic_invoice_draft + public function getInvoiceDraft(int $draft_invoice_number, string $currency = 'DKK', bool $skip_fetch = false): economic_invoice_draft { - return new $this->helpers->economic_invoice_draft($draft_invoice_number, $currency); + return new $this->helpers->economic_invoice_draft($draft_invoice_number, $currency, $skip_fetch); } /** diff --git a/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php b/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php index 935b3f51..45e92b29 100644 --- a/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php +++ b/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php @@ -61,7 +61,7 @@ class economic_invoices_draft_endpoint */ public function add_order(int $invoiceDraftId, orders_o $order, string $currency = 'DKK'): void { - $draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency)); + $draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true); // Add the transaction header (Timestamp, department, etc.) $draftInvoice->addNewTransactionHeader($order); // Add the order lines diff --git a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php index 4c23df5b..74013534 100644 --- a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php +++ b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php @@ -45,11 +45,17 @@ class economic_invoice_draft * Construct a new Economic draft invoice object * @throws Exception if the invoice data is invalid or empty */ - public function __construct(int $draft_invoice_number, string $currency = 'DKK') + public function __construct(int $draft_invoice_number, string $currency = 'DKK', bool $skip_fetch = false) { $this->setDraftInvoiceNumber($draft_invoice_number); $this->setCurrency(strtoupper($currency)); - $this->fetchDraftInvoiceData(); + // If the skip fetch is set to false, fetch the draft invoice data + if (!$skip_fetch) { + $this->fetchDraftInvoiceData(); + } else { + $this->draft_invoice_data = new \stdClass(); + $this->draft_invoice_data->draftInvoiceNumber = $this->draft_invoice_number; + } $this->requireSelected(); } diff --git a/services/nginx/app/objects/collected_order_invoices_o.php b/services/nginx/app/objects/collected_order_invoices_o.php index 0fe53e7c..3735c6c3 100644 --- a/services/nginx/app/objects/collected_order_invoices_o.php +++ b/services/nginx/app/objects/collected_order_invoices_o.php @@ -360,9 +360,14 @@ class collected_order_invoices_o extends db // Create an economic draft self::createInvoiceDraft(); + + // Check if the invoice draft exists + if (!self::isDraftExisting()) { + throw new Exception('Failed to create invoice draft'); + } // Add the invoices to the invoice draft - self::addInvoicesToDraft(); + self::addInvoicesToDraft(true); // Close the invoice collection // If the invoice collection is closed, we don't want to close it again @@ -534,17 +539,20 @@ class collected_order_invoices_o extends db /** * Add the invoices to the invoice draft - * @throws Exception If the request was not successful + * @param bool $skip_check If the check for the invoice collection being booked should be skipped * @throws Exception If the invoice collection is not set * @throws Exception If the invoice collection is already closed * @throws Exception If the invoice draft was not found + * @throws Exception If the request was not successful */ - public function addInvoicesToDraft(): self + public function addInvoicesToDraft(bool $skip_check = false): self { // Require the invoice collection to be selected self::requireSelected(); // Require the invoice collection to be open self::requireInvoiceIsNotBooked(); + // Set the timeout to 0, to prevent the script from timing out + set_time_limit(0); // Get the orders in the invoice collection $orders = self::getOrders(); // Check if there are any orders in the invoice collection @@ -552,11 +560,16 @@ class collected_order_invoices_o extends db throw new Exception('No orders in invoice collection'); } // Require the invoice draft to be set (and exists) - self::requireInvoiceDraft(); - + if (!$skip_check) { + self::requireInvoiceDraft(); + } + // Get the invoice draft id from the external id + $draft_id = self::getInvoiceDraftId(); + // Get the customer currency + $currency = self::getCustomerCurrency($this->customer_number->value()); // Add the invoices to the invoice draft foreach ( $orders as $order ) { - self::addInvoiceToDraft($order['id']); + self::addInvoiceToDraft($order['id'], true, $draft_id, $currency); } return $this; @@ -612,38 +625,6 @@ class collected_order_invoices_o extends db } } - /** - * Add an invoice to the invoice draft - * @param int $order_id The order id to add - * @throws Exception If the request was not successful - * @throws Exception If the invoice collection is not set - * @throws Exception If the invoice collection is already closed - * @throws Exception If the invoice draft was not found - */ - public function addInvoiceToDraft(int $order_id): self - { - // Require the invoice collection to be selected - self::requireSelected(); - // Require the invoice collection to be open - self::requireInvoiceIsNotBooked(); - // Get the order object - $order = new orders_o(); - $order->select($order_id); - $order->requireSelected(); - // Require the invoice draft to be set (and exists) - self::requireInvoiceDraft(); - - // Add the invoice to the invoice draft - $economic = new economic(); - $economic->invoices->draft->add_order( - self::getInvoiceDraftId(), - $order, - (string)self::getCustomerCurrency($order->customer_id->value()), - ); - - return $this; - } - /** * Get the customer currency * @return string The customer currency @@ -660,6 +641,52 @@ class collected_order_invoices_o extends db return $customer->getCurrency(); } + /** + * Add an invoice to the invoice draft + * @param int $order_id The order id to add + * @param bool $skip_check If the check for the invoice collection being booked should be skipped + * @param string $currency The currency to use (default: DKK) + * @throws Exception If the request was not successful + * @throws Exception If the invoice collection is not set + * @throws Exception If the invoice collection is already closed + * @throws Exception If the invoice draft was not found + */ + public function addInvoiceToDraft(int $order_id, bool $skip_check = false, int $draft_id = null, string $currency = null): self + { + // Require the invoice collection to be selected + self::requireSelected(); + // Get the order object + $order = new orders_o(); + $order->select($order_id); + $order->requireSelected(); + // Require the invoice draft to be set (and exists) + if (!$skip_check) { + // Require the invoice collection to be open + self::requireInvoiceIsNotBooked(); + // Require the invoice draft to be set + self::requireInvoiceDraft(); + } + // Get the invoice draft id from the external id, if not set + if (empty($draft_id)) { + $draft_id = self::getInvoiceDraftId(); + } + + // If the currency is not set, get the customer currency + if (empty($currency)) { + $currency = (string)self::getCustomerCurrency($order->customer_id->value()); + } + + // Add the invoice to the invoice draft + $economic = new economic(); + $economic->invoices->draft->add_order( + $draft_id, + $order, + (string)$currency + ); + + return $this; + } + /** * Close the invoice collection * @throws Exception If the request was not successful diff --git a/services/nginx/app/objects/currency_conversion_rates_o.php b/services/nginx/app/objects/currency_conversion_rates_o.php index 9381153e..7f745bc0 100644 --- a/services/nginx/app/objects/currency_conversion_rates_o.php +++ b/services/nginx/app/objects/currency_conversion_rates_o.php @@ -185,6 +185,10 @@ class currency_conversion_rates_o extends db if (!self::validateCurrencyFormat($currency)) { throw new Exception('The currency is not valid.'); } + // Check if the currency is 'DKK', then return 1.0 (base currency) + if ($currency === 'DKK') { + return 1.0; + } // Get the conversion rate $result = self::getFieldsWhere(['currency' => $currency], ['rate']); if (empty($result)) { diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index 4dd65224..56884d35 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -259,7 +259,7 @@ class orders_o extends db { $order = new orders_o(); $order->getOrderById($orderId); - return (new users_o())->getCustomerByIdOrCustomerNumber($order->customer_id->value()); + return (new users_o())->getUserByCustomerNumber($order->customer_id->value()); } public function getOrderById(int $id): orders_o diff --git a/services/nginx/app/objects/user_key_value_pairs_o.php b/services/nginx/app/objects/user_key_value_pairs_o.php index 62023385..1c9c61e7 100644 --- a/services/nginx/app/objects/user_key_value_pairs_o.php +++ b/services/nginx/app/objects/user_key_value_pairs_o.php @@ -108,4 +108,28 @@ class user_key_value_pairs_o extends db return $keys; } + public function getCustomerNumbersWithKey(array $keys): array + { + global $db; + // Get all the users, and their customer numbers, with the given keys + $sql = "SELECT user_id FROM $this->table WHERE var IN ('" . implode("','", $keys) . "')"; + $result = $db->query($sql); + $result = $db->fetch_all($result); + $user_ids = []; + // Add the user ids to the array + foreach ( $result as $row ) { + $user_ids[] = (int)$row['user_id']; + } + // Get the customer numbers from the users table + $sql = "SELECT id, customer_number FROM users WHERE id IN (" . implode(',', $user_ids) . ")"; + $result = $db->query($sql); + $result = $db->fetch_all($result); + $customer_numbers = []; + // Return the customer numbers + foreach ( $result as $row ) { + $customer_numbers[] = (int)$row['customer_number']; + } + return $customer_numbers; + } + } \ No newline at end of file diff --git a/services/nginx/app/routes/orderInvoicesRoute.php b/services/nginx/app/routes/orderInvoicesRoute.php index 492e2b49..4b02d52b 100644 --- a/services/nginx/app/routes/orderInvoicesRoute.php +++ b/services/nginx/app/routes/orderInvoicesRoute.php @@ -306,7 +306,7 @@ class orderInvoicesRoute }, $users_o->forceRestrictFilters( [ - 'active_invoices' => 'NOT ZERO', + //'active_invoices' => 'NOT ZERO', ] ) ); @@ -349,7 +349,7 @@ class orderInvoicesRoute }, $users_o->forceRestrictFilters( [ - 'active_invoices' => 'NOT ZERO', + //'active_invoices' => 'NOT ZERO', ] ) ); diff --git a/services/nginx/app/routes/ordersRoute.php b/services/nginx/app/routes/ordersRoute.php index 64f37091..3551d352 100644 --- a/services/nginx/app/routes/ordersRoute.php +++ b/services/nginx/app/routes/ordersRoute.php @@ -33,6 +33,7 @@ class ordersRoute $economic_module_orders = new economic_module_orders(); $orders = new orders_o(); // Return the list of departments + $orders->setView('orders_with_invoice_collections'); $response->success( $orders->listObjectsWithPaginationIfSet( function ($order) { diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 0b8f027b..6f74bd32 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -220,7 +220,6 @@ trait db_object_t foreach ( $temp as $key => $value ) { if (!is_array($value) && strtolower($value) === 'null') { $temp[$key] = null; - continue; } } $filters = $temp; @@ -319,7 +318,7 @@ trait db_object_t continue; } // If the value is null, add a where clause to check if the field is null - if ($value === null || (is_string($value) && strtolower($value) === 'null')) { + if ($value === null || (is_string($value) && strtolower($value) === 'null') || (is_string($value) && strtolower($value === 'is_null'))) { $whereClauses[] = "$field IS NULL"; continue; } @@ -335,6 +334,78 @@ trait db_object_t } $whereClauses[] = "`$field` = ?"; $params[] = $value; + } else { + // Add support for 'date_from' and 'date_to' filters + // Example of date_from: 'date_from:2023-01-01' + // Example of date_to: 'date_to:2023-12-31' + // In total the filter string would look like: 'created_at-date_from:2023-01-01,created_at-date_to:2023-12-31' + if (preg_match('/^(.+?)-(date_from|date_to)/', $field, $matches)) { + $fieldName = $matches[1]; + $filterType = $matches[2]; + $dateValue = $value; + // If the (date_to|date_from) is E.g. '2023-12-31', then we need to include the entire from/to date + // If the date is to be inclusive, we need to add 1 day to the date + if ($filterType === 'date_to') { + $dateValue = date('Y-m-d', strtotime($dateValue . ' +1 day')); + } + + if (in_array($fieldName, $fields)) { + if ($filterType === 'date_from') { + $whereClauses[] = "`$fieldName` >= ?"; + } elseif ($filterType === 'date_to') { + $whereClauses[] = "`$fieldName` <= ?"; + } + $params[] = $dateValue; + } + } elseif (preg_match('/^(.+?)-has_attribute/', $field, $matches)) { + // If the filter is e.g. customer_number-has_attribute:invoiceOrdersIndividually + // Get the field name and the attribute + $fieldName = $matches[1]; + // Set the attribute to the value, without the (optional) "!" prefix + $attribute = (string)str_replace('!', '', $value); + $state = !str_starts_with($value, '!'); + // Check if the field name is valid + if (!in_array($fieldName, $fields)) { + throw new Exception('Invalid filter: ' . $field); + } + $attributes = array(); + // Push the attribute to the array + $attributes[] = (string)$attribute; + // Get the customer numbers with the attribute + $tmp_customer_numbers_with_attribute = (new users_o())->getCustomerNumbersWithAttributes($attributes); + // If the state is true, add the customer numbers to the where clause + if ($state) { + $whereClauses[] = "$fieldName IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; + } else { + // If the state is false, add the customer numbers to the where clause + $whereClauses[] = "$fieldName NOT IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; + } + } elseif (preg_match('/^(.+?)-has_key/', $field, $matches)) { + // If the filter is e.g. customer_number-has_key:invoiceOrdersIndividually + // Get the field name and the key + $fieldName = $matches[1]; + // Set the key to the value, without the (optional) "!" prefix + $key = (string)str_replace('!', '', $value); + $state = !str_starts_with($value, '!'); + // Check if the field name is valid + if (!in_array($fieldName, $fields)) { + throw new Exception('Invalid filter: ' . $field); + } + $attributes = array(); + // Push the attribute to the array + $attributes[] = (string)$key; + // Get the customer numbers with the attribute + $tmp_customer_numbers_with_attribute = (new user_key_value_pairs_o())->getCustomerNumbersWithKey($attributes); + // If the state is true, add the customer numbers to the where clause + if ($state) { + $whereClauses[] = "$fieldName IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; + } else { + // If the state is false, add the customer numbers to the where clause + $whereClauses[] = "$fieldName NOT IN (" . implode(',', array_map('intval', $tmp_customer_numbers_with_attribute)) . ")"; + } + } else { + throw new Exception('Invalid filter: ' . $field); + } } } } diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index eba20470..24da1de0 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -72,6 +72,7 @@ http { include fastcgi_params; fastcgi_pass php:9000; fastcgi_index index.php; + fastcgi_read_timeout 600; fastcgi_param SCRIPT_FILENAME index.php; }