501 lines
18 KiB
PHP
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');
|
|
}
|
|
}
|
|
}
|