291 lines
11 KiB
PHP
291 lines
11 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use helpers\economic_invoice_booked;
|
|
use objects\collected_order_invoices_o;
|
|
use objects\order_items_o;
|
|
use objects\orders_o;
|
|
use objects\products_o;
|
|
|
|
class economic_v2_line_normalizer
|
|
{
|
|
public static function normalizeInternalCollectedInvoice(collected_order_invoices_o $invoice): array
|
|
{
|
|
$lines = [];
|
|
$warnings = [];
|
|
|
|
foreach ($invoice->getOrders() as $order_row) {
|
|
$order = (new orders_o())->select((int)$order_row['id']);
|
|
$department_id = (int)$order->department_id->value();
|
|
$order_items = (new order_items_o())->getAllItemsAsArray(
|
|
(int)$order->id,
|
|
['id', 'product_id', 'reference', 'notes', 'price', 'quantity', 'include_in_invoice']
|
|
);
|
|
|
|
foreach ($order_items as $row) {
|
|
if (!(bool)($row['include_in_invoice'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
$product = (new products_o())->getProductById((int)$row['product_id']);
|
|
if (!$product->exists()) {
|
|
$warnings[] = 'Missing product for internal order item id ' . (int)$row['id'];
|
|
continue;
|
|
}
|
|
|
|
$product_number = $product->economic_product_id->value();
|
|
$quantity = (float)$row['quantity'];
|
|
$unit_net_price = (float)$row['price'];
|
|
$line_net_amount = $quantity * $unit_net_price;
|
|
$department_distribution = [$department_id => 100.0];
|
|
|
|
$line = [
|
|
'index' => count($lines),
|
|
'source' => 'internal',
|
|
'source_order_id' => (int)$order->id,
|
|
'source_line_id' => (int)$row['id'],
|
|
'line_type' => self::detectLineType($product_number, $unit_net_price, true),
|
|
'billable' => true,
|
|
'product_number' => $product_number !== null ? (string)$product_number : null,
|
|
'product_id' => (int)$row['product_id'],
|
|
'description' => (string)$product->name->value(),
|
|
'reference' => (string)($row['reference'] ?? ''),
|
|
'quantity' => $quantity,
|
|
'unit_net_price' => $unit_net_price,
|
|
'line_net_amount' => $line_net_amount,
|
|
'department_distribution' => $department_distribution,
|
|
];
|
|
$line['match_key'] = self::buildMatchKey($line);
|
|
$lines[] = $line;
|
|
}
|
|
}
|
|
|
|
return self::wrap('internal', $lines, $warnings);
|
|
}
|
|
|
|
public static function normalizeDraftInvoice(object|array|null $draft_invoice): array
|
|
{
|
|
if ($draft_invoice === null) {
|
|
return self::wrap('draft', [], ['Draft invoice missing']);
|
|
}
|
|
|
|
$data = self::toArray($draft_invoice);
|
|
$raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
|
|
$lines = [];
|
|
|
|
foreach ($raw_lines as $raw_line) {
|
|
$line = self::normalizeEconomicLine($raw_line, 'draft');
|
|
$line['index'] = count($lines);
|
|
$line['source_line_id'] = isset($raw_line['lineNumber']) ? (int)$raw_line['lineNumber'] : (int)($raw_line['line_number'] ?? count($lines) + 1);
|
|
$line['match_key'] = self::buildMatchKey($line);
|
|
$lines[] = $line;
|
|
}
|
|
|
|
$wrapped = self::wrap('draft', $lines, []);
|
|
if (isset($data['netAmount'])) {
|
|
$wrapped['totals']['net_total'] = (float)$data['netAmount'];
|
|
} elseif (isset($data['net_amount'])) {
|
|
$wrapped['totals']['net_total'] = (float)$data['net_amount'];
|
|
}
|
|
$wrapped['totals']['difference_from_line_sum'] = round(
|
|
(float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
|
|
5
|
|
);
|
|
|
|
return $wrapped;
|
|
}
|
|
|
|
public static function normalizeBookedInvoice(object|array|null $booked_invoice): array
|
|
{
|
|
if ($booked_invoice === null) {
|
|
return self::wrap('booked', [], ['Booked invoice missing']);
|
|
}
|
|
|
|
if ($booked_invoice instanceof economic_invoice_booked) {
|
|
$data = $booked_invoice->toArray();
|
|
} else {
|
|
$data = self::toArray($booked_invoice);
|
|
}
|
|
|
|
$raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
|
|
$lines = [];
|
|
foreach ($raw_lines as $raw_line) {
|
|
$line = self::normalizeEconomicLine($raw_line, 'booked');
|
|
$line['index'] = count($lines);
|
|
$line['source_line_id'] = (int)($raw_line['lineNumber'] ?? $raw_line['line_number'] ?? count($lines) + 1);
|
|
$line['match_key'] = self::buildMatchKey($line);
|
|
$lines[] = $line;
|
|
}
|
|
|
|
$wrapped = self::wrap('booked', $lines, []);
|
|
if (isset($data['netAmount'])) {
|
|
$wrapped['totals']['net_total'] = (float)$data['netAmount'];
|
|
} elseif (isset($data['net_amount'])) {
|
|
$wrapped['totals']['net_total'] = (float)$data['net_amount'];
|
|
}
|
|
$wrapped['totals']['difference_from_line_sum'] = round(
|
|
(float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
|
|
5
|
|
);
|
|
|
|
return $wrapped;
|
|
}
|
|
|
|
private static function normalizeEconomicLine(array $raw_line, string $source): array
|
|
{
|
|
$line = self::toArray($raw_line);
|
|
|
|
$product_number = $line['product']['productNumber']
|
|
?? $line['product']['product_number']
|
|
?? null;
|
|
$description = (string)($line['description'] ?? '');
|
|
$quantity = isset($line['quantity']) ? (float)$line['quantity'] : 0.0;
|
|
$unit_net_price = isset($line['unitNetPrice'])
|
|
? (float)$line['unitNetPrice']
|
|
: (isset($line['unit_net_price']) ? (float)$line['unit_net_price'] : 0.0);
|
|
$line_net_amount = isset($line['totalNetAmount'])
|
|
? (float)$line['totalNetAmount']
|
|
: (isset($line['total_net_amount']) ? (float)$line['total_net_amount'] : $quantity * $unit_net_price);
|
|
|
|
$department_distribution = self::extractDepartmentDistribution($line);
|
|
$billable = ($product_number !== null) || abs($line_net_amount) > 0.00001 || abs($quantity) > 0.00001;
|
|
|
|
return [
|
|
'source' => $source,
|
|
'source_order_id' => null,
|
|
'line_type' => self::detectLineType($product_number, $unit_net_price, $billable),
|
|
'billable' => $billable,
|
|
'product_number' => $product_number !== null ? (string)$product_number : null,
|
|
'product_id' => null,
|
|
'description' => $description,
|
|
'reference' => '',
|
|
'quantity' => $quantity,
|
|
'unit_net_price' => $unit_net_price,
|
|
'line_net_amount' => $line_net_amount,
|
|
'department_distribution' => $department_distribution,
|
|
];
|
|
}
|
|
|
|
private static function extractDepartmentDistribution(array $line): array
|
|
{
|
|
$distribution = [];
|
|
$dd = $line['departmentalDistribution'] ?? $line['departmental_distribution'] ?? null;
|
|
if (is_array($dd)) {
|
|
$distributions = $dd['distributions'] ?? null;
|
|
if (is_array($distributions)) {
|
|
foreach ($distributions as $entry) {
|
|
$department_number = $entry['department']['departmentNumber']
|
|
?? $entry['department']['department_number']
|
|
?? null;
|
|
if ($department_number === null) {
|
|
continue;
|
|
}
|
|
$distribution[(string)$department_number] = (float)($entry['percentage'] ?? 0.0);
|
|
}
|
|
}
|
|
|
|
if (empty($distribution) && isset($dd['departmentalDistributionNumber'])) {
|
|
$distribution[(string)$dd['departmentalDistributionNumber']] = 100.0;
|
|
} elseif (empty($distribution) && isset($dd['departmental_distribution_number'])) {
|
|
$distribution[(string)$dd['departmental_distribution_number']] = 100.0;
|
|
}
|
|
}
|
|
|
|
if (empty($distribution)) {
|
|
$distribution['unassigned'] = 100.0;
|
|
}
|
|
|
|
return $distribution;
|
|
}
|
|
|
|
private static function wrap(string $source, array $lines, array $warnings): array
|
|
{
|
|
$net_total = 0.0;
|
|
$line_net_total = 0.0;
|
|
$billable_count = 0;
|
|
$departments = [];
|
|
|
|
foreach ($lines as $line) {
|
|
$line_net_total += (float)$line['line_net_amount'];
|
|
if (!(bool)$line['billable']) {
|
|
continue;
|
|
}
|
|
$billable_count++;
|
|
$line_amount = (float)$line['line_net_amount'];
|
|
$net_total += $line_amount;
|
|
|
|
foreach ($line['department_distribution'] as $department_key => $percentage) {
|
|
if (!isset($departments[$department_key])) {
|
|
$departments[$department_key] = 0.0;
|
|
}
|
|
$departments[$department_key] += $line_amount * ((float)$percentage / 100);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'source' => $source,
|
|
'totals' => [
|
|
'net_total' => round($net_total, 5),
|
|
'line_net_total' => round($line_net_total, 5),
|
|
'line_count' => count($lines),
|
|
'billable_line_count' => $billable_count,
|
|
],
|
|
'departments' => self::roundMap($departments),
|
|
'lines' => $lines,
|
|
'warnings' => $warnings,
|
|
];
|
|
}
|
|
|
|
private static function buildMatchKey(array $line): string
|
|
{
|
|
if (!empty($line['product_number'])) {
|
|
return 'product:' . strtolower(trim((string)$line['product_number'])) .
|
|
'|ref:' . strtolower(trim((string)($line['reference'] ?? '')));
|
|
}
|
|
|
|
return 'text:' . self::normalizeText((string)$line['description']);
|
|
}
|
|
|
|
private static function detectLineType(mixed $product_number, float $unit_net_price, bool $billable): string
|
|
{
|
|
if (!$billable) {
|
|
return 'text';
|
|
}
|
|
|
|
if ($product_number !== null && strtolower((string)$product_number) === 'totdiscount') {
|
|
return 'discount';
|
|
}
|
|
|
|
if ($unit_net_price < 0) {
|
|
return 'discount';
|
|
}
|
|
|
|
return $product_number !== null ? 'product' : 'text';
|
|
}
|
|
|
|
private static function normalizeText(string $text): string
|
|
{
|
|
$text = trim(strtolower($text));
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|
return $text ?? '';
|
|
}
|
|
|
|
private static function roundMap(array $map): array
|
|
{
|
|
$rounded = [];
|
|
foreach ($map as $k => $v) {
|
|
$rounded[(string)$k] = round((float)$v, 5);
|
|
}
|
|
return $rounded;
|
|
}
|
|
|
|
private static function toArray(object|array $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE), true) ?: [];
|
|
}
|
|
}
|