feat(123): receipts export xlsx VAT breakdown

- AccountingController::export(): new headers (Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT), removed Data sprzedazy/Konfiguracja/Nr zamowienia/Nr referencyjny
- buildVatBreakdown() helper groups items_json by vat rate, emits one XLSX row per (receipt x rate); legacy receipts (no `vat` in snapshot) fallback to net=brutto/1.23
- ReceiptService::buildItemsSnapshot(): writes `vat` per item from order_items.tax_rate (fallback 23.0); shipping cost item gets vat=23.0
- RECEIPT-NET-FIX deferred (.paul/codebase/todo.md): ReceiptService::issue() still saves total_net=total_gross

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:06:53 +02:00
parent a4ed4531dc
commit 0227f2d072
12 changed files with 491 additions and 17 deletions

View File

@@ -130,7 +130,7 @@ final class AccountingController
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Paragony');
$headers = ['Numer', 'Data wystawienia', 'Data sprzedazy', 'Kwota brutto', 'Konfiguracja', 'Nr zamowienia', 'Nr referencyjny'];
$headers = ['Numer', 'Data wystawienia', 'Kwota brutto', 'Kwota netto', 'Stawka VAT', 'Kwota VAT'];
foreach ($headers as $col => $header) {
$sheet->setCellValue([$col + 1, 1], $header);
}
@@ -139,18 +139,26 @@ final class AccountingController
$rowNum = 2;
foreach ($rows as $row) {
$sheet->setCellValue([1, $rowNum], (string) ($row['receipt_number'] ?? ''));
$issueDateRaw = (string) ($row['issue_date'] ?? '');
$sheet->setCellValue([2, $rowNum], strlen($issueDateRaw) >= 16 ? substr($issueDateRaw, 0, 16) : $issueDateRaw);
$sheet->setCellValue([3, $rowNum], (string) ($row['sale_date'] ?? ''));
$sheet->setCellValue([4, $rowNum], (float) ($row['total_gross'] ?? 0));
$sheet->setCellValue([5, $rowNum], (string) ($row['config_name'] ?? ''));
$sheet->setCellValue([6, $rowNum], (string) ($row['internal_order_number'] ?? $row['external_order_id'] ?? ''));
$sheet->setCellValue([7, $rowNum], (string) ($row['order_reference_value'] ?? ''));
$rowNum++;
$issueDate = strlen($issueDateRaw) >= 16 ? substr($issueDateRaw, 0, 16) : $issueDateRaw;
$receiptNumber = (string) ($row['receipt_number'] ?? '');
$totalGross = (float) ($row['total_gross'] ?? 0);
$totalNet = (float) ($row['total_net'] ?? 0);
$breakdown = $this->buildVatBreakdown((string) ($row['items_json'] ?? ''), $totalNet, $totalGross);
foreach ($breakdown as $line) {
$sheet->setCellValue([1, $rowNum], $receiptNumber);
$sheet->setCellValue([2, $rowNum], $issueDate);
$sheet->setCellValue([3, $rowNum], $totalGross);
$sheet->setCellValue([4, $rowNum], $line['net']);
$sheet->setCellValue([5, $rowNum], $line['rate_label']);
$sheet->setCellValue([6, $rowNum], $line['vat']);
$rowNum++;
}
}
foreach (range(1, 7) as $col) {
foreach (range(1, 6) as $col) {
$sheet->getColumnDimensionByColumn($col)->setAutoSize(true);
}
@@ -235,4 +243,71 @@ final class AccountingController
return $params !== [] ? '?' . http_build_query($params) : '';
}
/**
* @return list<array{rate_label: string, net: float, vat: float}>
*/
private function buildVatBreakdown(string $itemsJson, float $totalNet, float $totalGross): array
{
$items = [];
if ($itemsJson !== '') {
$decoded = json_decode($itemsJson, true);
if (is_array($decoded)) {
$items = $decoded;
}
}
$groups = [];
$hasVat = false;
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
if (!array_key_exists('vat', $item) || $item['vat'] === null || $item['vat'] === '') {
continue;
}
$hasVat = true;
$vatRate = (float) $item['vat'];
$key = number_format($vatRate, 2, '.', '');
$lineGross = (float) ($item['total'] ?? 0);
if (!isset($groups[$key])) {
$groups[$key] = ['rate' => $vatRate, 'gross' => 0.0];
}
$groups[$key]['gross'] += $lineGross;
}
if (!$hasVat) {
$net = round($totalGross / 1.23, 2);
$vatAmount = round($totalGross - $net, 2);
return [[
'rate_label' => '23%',
'net' => $net,
'vat' => $vatAmount,
]];
}
ksort($groups, SORT_NUMERIC);
$result = [];
foreach (array_reverse($groups) as $group) {
$rate = (float) $group['rate'];
$gross = (float) $group['gross'];
$net = $rate > 0.0 ? round($gross / (1 + $rate / 100), 2) : round($gross, 2);
$vat = round($gross - $net, 2);
$result[] = [
'rate_label' => $this->formatVatRate($rate),
'net' => $net,
'vat' => $vat,
];
}
return $result;
}
private function formatVatRate(float $rate): string
{
if (abs($rate - round($rate)) < 0.005) {
return ((int) round($rate)) . '%';
}
return rtrim(rtrim(number_format($rate, 2, '.', ''), '0'), '.') . '%';
}
}

View File

@@ -261,11 +261,14 @@ final class ReceiptService
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
$lineTotal = $qty * $price;
$totalGross += $lineTotal;
$vatRaw = $item['tax_rate'] ?? $item['vat'] ?? null;
$vat = $vatRaw !== null && $vatRaw !== '' ? (float) $vatRaw : 23.0;
$itemsSnapshot[] = [
'name' => $item['original_name'] ?? '',
'quantity' => $qty,
'price' => $price,
'total' => $lineTotal,
'vat' => $vat,
'sku' => $item['sku'] ?? '',
'ean' => $item['ean'] ?? '',
];
@@ -279,6 +282,7 @@ final class ReceiptService
'quantity' => 1.0,
'price' => $deliveryPrice,
'total' => $deliveryPrice,
'vat' => 23.0,
'sku' => '',
'ean' => '',
];