setDraftInvoiceNumber($draft_invoice_number); $this->setCurrency(strtoupper($currency)); // 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(); } /** * Set the draft invoice number * @param int $draft_invoice_number * @return void */ private function setDraftInvoiceNumber(int $draft_invoice_number): void { $this->draft_invoice_number = $draft_invoice_number; } /** * Set the currency for the draft invoice * @param string $currency The currency to set * @return void * @throws Exception If the currency is not valid * @throws Exception If the conversion rate is not valid */ private function setCurrency(string $currency): void { // Set the currency for the draft invoice $this->currency = $currency; // Get the conversion rate for the currency $this->conversion_rate = (float)(new currency_conversion_rates_o())->convertTo($this->currency, 1); } /** * Fetch the draft invoice data from the Economic system * @throws Exception If the request fails * @throws Exception If the invoice is not found */ private function fetchDraftInvoiceData(): void { $economic = new economic(); $this->draft_invoice_data = $economic->invoices->draft->get($this->draft_invoice_number); if (!isset($this->draft_invoice_data->draftInvoiceNumber)) { throw new Exception('Draft invoice not found'); } } /** * Require that the draft invoice data is selected * @throws Exception if the draft invoice data is not set */ protected function requireSelected(): void { if (!isset($this->draft_invoice_data->draftInvoiceNumber)) { throw new Exception('The draft invoice data is not set'); } } /** * Add the lines to the draft invoice. */ public function addLines(): void { $this->flushLinesInBatches(); } /** * Add queued draft lines using chunked requests. * * @return array{line_count:int,batch_count:int,batch_sizes:array} */ public function flushLinesInBatches(int $batch_size = self::DEFAULT_LINE_BATCH_SIZE): array { $lines = array_values($this->draft_lines); $line_count = count($lines); if ($line_count === 0) { return [ 'line_count' => 0, 'batch_count' => 0, 'batch_sizes' => [], ]; } $batch_size = max(1, $batch_size); $batch_sizes = []; foreach (array_chunk($lines, $batch_size) as $batch) { $this->sendDraftLines($batch); $batch_sizes[] = count($batch); } $this->draft_lines = []; return [ 'line_count' => $line_count, 'batch_count' => count($batch_sizes), 'batch_sizes' => $batch_sizes, ]; } public function pendingLineCount(): int { return count($this->draft_lines); } protected function sendDraftLines(array $draft_lines): object { $economic = new economic(); return $economic->invoices->draft->add_lines($this->draft_invoice_number, $draft_lines); } /** * Get the draft invoice number * @return int The draft invoice number */ public function getDraftInvoiceNumber(): int { return $this->draft_invoice_number; } /** * Add a new transaction header to the draft invoice * @note The lines won't be saved until the addLines() method is called. * @note This is the "[28/02/2025 18:29 Roskilde #685]" line in the invoice * @param orders_o $order The order to add * @return void */ public function addNewTransactionHeader(orders_o $order): void { // Get the department name $department_name = (new departments_o())->getDepartmentName((int)$order->department_id->value()); // Parse the date of the transaction. $parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value())); // Add the text line to the draft invoice self::addTextLine("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]"); // If there's a PO number, add it to the invoice if ($order->po->value() !== '') { self::addTextLine('PO: ' . $order->po->value()); } // If there's a reference, add it to the invoice if ($order->reference->value() !== '') { self::addTextLine('Reference:'); // Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once) if (str_contains($order->reference->value(), "\n")) { foreach ( explode("\n", $order->reference->value()) as $line ) { self::addTextLine('# ' . $line); } } else { self::addTextLine('# ' . $order->reference->value()); } } // Add the registration numbers (if any) $line_reg = ''; if ($order->reg_1->value() !== '') $line_reg .= 'Reg 1: ' . strtoupper($order->reg_1->value()); if ($order->reg_2->value() !== '') $line_reg .= ', Reg 2: ' . strtoupper($order->reg_2->value()); if ($order->reg_3->value() !== '') $line_reg .= ', Reg 3: ' . strtoupper($order->reg_3->value()); // Add the line to the invoice (If there's any registration numbers) if ($line_reg !== '') self::addTextLine($line_reg); // If there's a note, add it to the invoice if ($order->notes->value() !== '') { self::addTextLine('Notat:'); // Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once) if (str_contains($order->notes->value(), "\n")) { foreach ( explode("\n", $order->notes->value()) as $line ) { self::addTextLine('# ' . $line); } } else { self::addTextLine('# ' . $order->notes->value()); } } } /** * Add a text line to the draft invoice * @note The lines won't be saved until the addLines() method is called. * @param string $text The text to add * @return void */ public function addTextLine(string $text): void { $this->draft_lines[] = [ 'description' => $text ]; } /** * Add an order to the draft invoice * @note The lines won't be saved until the addLines() method is called. * @param orders_o $order The order to add * @return void * @throws Exception if the order is not found * @throws Exception if the order is not valid */ public function addOrderItemLines(orders_o $order): void { // Get the order items $order_items = $order->getOrderItems($order->id); // Apply the department pricing $order_items = $order->applyDepartmentPrices($order_items, $order->department_id->value()); // Get the department $department = $order->getDepartmentByOrderId($order->id); // If the department id is 10, check if the user has a default department set if ((int)$department['id'] === 10) { // If the default department is set, use it $department = (new departments_o())->getDepartmentById((int)$department['id'], true); } // Remove all the order items that are not included in the invoice $order_items = array_filter($order_items, function ($order_item) { return (bool)$order_item['include_in_invoice']; }); // Define the total discount applied to the order $total_discount = 0; // Loop through the order items foreach ( $order_items as $order_item ) { if ($this->shouldSkipOrderItemLine($order_item)) { continue; } // Add the order item to the draft invoice self::addOrderItemLine($order_item, $department); // Add the line discount to the total discount $total_discount += ($order_item['product']['price'] - $order_item['price']) * $order_item['quantity']; } // If the total discount is greater than 0, add it to the invoice if ($total_discount > 0) { // Add the discount to the invoice self::addProductDiscountLine($total_discount, $department['economic_department_id'] ?? 0, $department['dimension'] ?? 0); } } /** * Add an order item line to the draft invoice * @note The lines won't be saved until the addLines() method is called. * @param array $order_item The order item to add * @param array $department The department to add * @param boolean $show_discount Whether to show the discount or not * @return void * @throws Exception if the order item is not found * @throws Exception if the order item is not valid */ public function addOrderItemLine(array $order_item, array $department, bool $show_discount = false): void { // Check if the order item is valid if (!isset($order_item['id'])) { throw new Exception('The order item is not valid'); } if ($this->shouldSkipOrderItemLine($order_item)) { return; } // Get the department id $economic_department_id = $department['economic_department_id'] ?? 0; // Get the dimension id $economic_dimension_id = $department['economic_dimension_id'] ?? 0; // Add the order item to the draft invoice self::addProductLine( (string)$order_item['product']['economic_product_id'], (string)$order_item['product']['name'], (int)$order_item['quantity'] ?? 1, (int)($order_item['product']['price'] == 0 ? $order_item['price'] : $order_item['product']['price']), (int)$economic_department_id, $economic_dimension_id ); // Calculate the discount percentage (If the final price is 0, set the discount percentage to 100) if ($order_item['product']['price'] == 0) { $show_discount = false; $discountPercentage = 0; } else { // Calculate the discount percentage $discountPercentage = round((($order_item['product']['price'] - $order_item['price']) / $order_item['product']['price']) * 100, 0); } // If the price is different from the product price, add it to the line if ($order_item['price'] !== $order_item['product']['price'] && $show_discount) self::addTextLine('Rabat: ' . ($order_item['price'] - $order_item['product']['price']) . ' ' . $this->currency . ' (' . $discountPercentage . '%)'); // If there's a reference, add it to the line if ($order_item['reference'] !== '') { self::addTextLine('Reference:'); // Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once) if (str_contains($order_item['reference'], "\n")) { foreach ( explode("\n", $order_item['reference']) as $line ) { self::addTextLine('# ' . $line); } } else { self::addTextLine('# ' . $order_item['reference']); } } // If there's a note, add it to the line if (!empty($order_item['notes'])) { self::addTextLine('Notat:'); // Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once) if (str_contains($order_item['notes'], "\n")) { foreach ( explode("\n", $order_item['notes']) as $line ) { self::addTextLine('# ' . $line); } } else { self::addTextLine('# ' . $order_item['notes']); } } } /** * Skip line when quantity is zero/negative, or final unit price is zero. */ private function shouldSkipOrderItemLine(array $order_item): bool { $quantity = isset($order_item['quantity']) && is_numeric($order_item['quantity']) ? (float)$order_item['quantity'] : 0.0; if ($quantity <= 0.0) { return true; } $price = isset($order_item['price']) && is_numeric($order_item['price']) ? (float)$order_item['price'] : 0.0; return abs($price) < 0.00001; } /** * Add a product line to the draft invoice * @note The lines won't be saved until the addLines() method is called. */ public function addProductLine(string $productNumber, string $description, float $quantity, float $unitNetPrice, int $economic_department_id, int $dimension): void { // We then add a 0 in front of the product number to make sure it's the right one, made by FlexPOS. // Add a line to the invoice $line = [ 'product' => [ 'productNumber' => $productNumber, ], 'quantity' => $quantity, 'unitNetPrice' => self::convertCurrency($unitNetPrice), 'discountPercentage' => 0, 'description' => $description, ]; // If the department is set, add it to the line if ($economic_department_id && $economic_department_id > 0) { $line['departmentalDistribution'] = [ 'departmentalDistributionNumber' => $economic_department_id, 'DistributionType' => 'Department', 'dimension' => $dimension ]; } $this->draft_lines[] = $line; } private function convertCurrency(float $amount): float { // Convert the amount to the correct currency $converted_value = $amount * self::getConversionRate(); // Round the value to 2 decimal places return round($converted_value, 2); } private function getConversionRate(): float { // Get the conversion rate for the currency return $this->conversion_rate; } /** * Add a discount line to the draft invoice * @note The lines won't be saved until the addLines() method is called. * @param float $discount The discount to add * @param int $economic_department_id The department id to add * @param int $economic_dimension_id The dimension id to add * @return void */ public function addProductDiscountLine(float $discount, int $economic_department_id, int $economic_dimension_id): void { // Add a line to the invoice $line = [ 'product' => [ 'productNumber' => 'TotDiscount', ], 'quantity' => 1, 'unitNetPrice' => self::convertCurrency(-$discount), 'discountPercentage' => 0, 'description' => 'Rabat', ]; // If the department is set, add it to the line if ($economic_department_id && $economic_department_id > 0) { $line['departmentalDistribution'] = [ 'departmentalDistributionNumber' => $economic_department_id, 'DistributionType' => 'Department', 'dimension' => $economic_dimension_id ]; } $this->draft_lines[] = $line; } /** * Check if a draft invoice exists in the Economic system * @throws Exception if the draft invoice does not exist */ protected function requireExists(): void { if (!self::exists()) { throw new Exception('The draft invoice does not exist'); } } /** * Check if a customer exists in the Economic system * @return bool True if the customer exists, false if not * @throws Exception */ public function exists(): bool { // Make sure the draft invoice number is set before calling self::requireDraftInvoiceNumber(); // Check if the draft invoice exists $economic = new economic(); return $economic->invoices->drafts->exists($this->draft_invoice_number); } /** * Require that the draft invoice number is set * @throws Exception if the draft invoice number is not set */ protected function requireDraftInvoiceNumber(): void { if (!isset($this->draft_invoice_number)) { throw new Exception('The draft invoice number is not set'); } } /** * Require that the draft invoice data is set * @throws Exception if the draft invoice data is not set */ protected function requireDraftInvoiceData(): void { if (!isset($this->draft_invoice_data)) { throw new Exception('The draft invoice data is not set'); } } }