Files
api/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php
T

501 lines
18 KiB
PHP

<?php
namespace helpers;
use classes\economic;
use Exception;
use objects\currency_conversion_rates_o;
use objects\departments_o;
use objects\orders_o;
class economic_invoice_draft
{
public const DEFAULT_LINE_BATCH_SIZE = 500;
/**
* The Economic draftInvoiceNumber
* @var int $draft_invoice_number
*/
protected int $draft_invoice_number;
/**
* The draft lines
* @var array $draft_lines
*/
protected array $draft_lines = [];
/**
* The raw draft invoice data
* @var object $draft_invoice_data
*/
protected object $draft_invoice_data;
/**
* The currency for the draft invoice
* @var string $currency
* @note The currency is set to DKK by default
*/
protected string $currency = 'DKK';
/**
* The currency conversion rate
* @var float $conversion_rate
* @note The conversion rate is set to 1.0 by default
*/
protected float $conversion_rate = 1.0;
/**
* 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', bool $skip_fetch = false)
{
$this->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<int,int>}
*/
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');
}
}
}