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:
@@ -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'), '.') . '%';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' => '',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user