53b3fd945a
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 5s
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 43s
Inventory: - General material, stock items, and sales CRUD - Category management with CategoryPicker component - Inventory reports service - Database migrations and mappers Frontend: - Inventory view with tabs (Allgemeinmaterial, Verkaufsmaterial, Verkäufe) - Forms for general material, stock items, and sales - Search bar fix: flexbox wrapper with inline icon replaces broken NcTextField icon slot Tests: - All 1,491 tests pass (zero errors, warnings, deprecations) - BundleImportServiceTest: fixed ZipArchive empty-file deprecation - BundleImportService: null-coalescing for targetFields - PHPUnit test infra: make test target via container CLAUDE.md: - Added Testing section as PR gating criterion
172 lines
6.0 KiB
PHP
172 lines
6.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace OCA\Mitgliederverwaltung\Service;
|
||
|
||
use OCA\Mitgliederverwaltung\Db\GeneralMaterialMapper;
|
||
use OCA\Mitgliederverwaltung\Db\InventoryCategoryMapper;
|
||
use OCA\Mitgliederverwaltung\Db\InventoryItemCategoryMapper;
|
||
use OCA\Mitgliederverwaltung\Db\StockItemMapper;
|
||
use OCA\Mitgliederverwaltung\Db\StockVariantMapper;
|
||
use OCA\Mitgliederverwaltung\Db\SaleRecordMapper;
|
||
use Psr\Log\LoggerInterface;
|
||
|
||
/**
|
||
* Service for generating inventory reports.
|
||
*
|
||
* Part of Issue #165 (Inventory Tracking).
|
||
*/
|
||
class InventoryReportService {
|
||
|
||
private GeneralMaterialMapper $generalMaterialMapper;
|
||
private InventoryCategoryMapper $categoryMapper;
|
||
private InventoryItemCategoryMapper $itemCategoryMapper;
|
||
private StockItemMapper $stockItemMapper;
|
||
private StockVariantMapper $stockVariantMapper;
|
||
private SaleRecordMapper $saleRecordMapper;
|
||
private LoggerInterface $logger;
|
||
|
||
public function __construct(
|
||
GeneralMaterialMapper $generalMaterialMapper,
|
||
InventoryCategoryMapper $categoryMapper,
|
||
InventoryItemCategoryMapper $itemCategoryMapper,
|
||
StockItemMapper $stockItemMapper,
|
||
StockVariantMapper $stockVariantMapper,
|
||
SaleRecordMapper $saleRecordMapper,
|
||
LoggerInterface $logger
|
||
) {
|
||
$this->generalMaterialMapper = $generalMaterialMapper;
|
||
$this->categoryMapper = $categoryMapper;
|
||
$this->itemCategoryMapper = $itemCategoryMapper;
|
||
$this->stockItemMapper = $stockItemMapper;
|
||
$this->stockVariantMapper = $stockVariantMapper;
|
||
$this->saleRecordMapper = $saleRecordMapper;
|
||
$this->logger = $logger;
|
||
}
|
||
|
||
/**
|
||
* Generate condition report data.
|
||
*
|
||
* @return array{summary: array, rows: array, headers: string[]}
|
||
*/
|
||
public function generateConditionReport(): array {
|
||
$items = $this->generalMaterialMapper->findAll();
|
||
|
||
// Build summary by category
|
||
$summary = [];
|
||
foreach ($items as $item) {
|
||
$catIds = $this->itemCategoryMapper->findByItemId($item->getId());
|
||
if (count($catIds) === 0) {
|
||
$catName = '(keine Kategorie)';
|
||
} else {
|
||
$catNames = [];
|
||
foreach ($catIds as $cat) {
|
||
try {
|
||
$c = $this->categoryMapper->findById((int)$cat->getCategoryId());
|
||
$catNames[] = $c->getName();
|
||
} catch (\Exception $e) {
|
||
// ignore
|
||
}
|
||
}
|
||
$catName = implode(', ', $catNames);
|
||
}
|
||
if (!isset($summary[$catName])) {
|
||
$summary[$catName] = 0;
|
||
}
|
||
$summary[$catName] += 1;
|
||
}
|
||
|
||
// Build detail rows for items needing repair (condition ≤ 2 or NULL)
|
||
$rows = [];
|
||
$needingRepair = $this->generalMaterialMapper->findNeedingRepair();
|
||
foreach ($needingRepair as $item) {
|
||
$catIds = $this->itemCategoryMapper->findByItemId($item->getId());
|
||
$catNames = [];
|
||
foreach ($catIds as $cat) {
|
||
try {
|
||
$c = $this->categoryMapper->findById((int)$cat->getCategoryId());
|
||
$catNames[] = $c->getName();
|
||
} catch (\Exception $e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
$conditionLabel = $item->getCondition() === null
|
||
? 'Nicht bewertet'
|
||
: $item->getCondition() . '/5';
|
||
|
||
$rows[] = [
|
||
'name' => $item->getName(),
|
||
'condition' => $conditionLabel,
|
||
'categories' => implode(', ', $catNames) ?: '–',
|
||
'notes' => $item->getNotes() ?? '–',
|
||
];
|
||
}
|
||
|
||
return [
|
||
'summary' => $summary,
|
||
'rows' => $rows,
|
||
'headers' => ['Artikel', 'Zustand', 'Kategorie', 'Notizen'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Generate sales report data.
|
||
*
|
||
* @param string|null $dateFrom Start date (YYYY-MM-DD)
|
||
* @param string|null $dateTo End date (YYYY-MM-DD)
|
||
* @return array{title: string, summary: array, rows: array, headers: string[]}
|
||
*/
|
||
public function generateSalesReport(?string $dateFrom = null, ?string $dateTo = null): array {
|
||
$sales = $this->saleRecordMapper->findByDateRange($dateFrom, $dateTo);
|
||
$rows = [];
|
||
$totalRevenue = 0.0;
|
||
|
||
foreach ($sales as $sale) {
|
||
$itemName = 'Unbekannt';
|
||
try {
|
||
$stockItem = $this->stockItemMapper->findById((int)$sale->getStockItemId());
|
||
$itemName = $stockItem->getName();
|
||
} catch (\Exception $e) {
|
||
// ignore, keep default name
|
||
}
|
||
|
||
$variantName = '';
|
||
if ($sale->getVariantId() !== null) {
|
||
try {
|
||
$variant = $this->stockVariantMapper->findById((int)$sale->getVariantId());
|
||
$variantName = ' – ' . $variant->getLabel();
|
||
} catch (\Exception $e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
$totalRevenue += (float)$sale->getTotalPrice();
|
||
|
||
$rows[] = [
|
||
'date' => $sale->getDate(),
|
||
'item' => $itemName . $variantName,
|
||
'quantity' => (string)$sale->getQuantity(),
|
||
'unit_price' => number_format((float)$sale->getUnitPrice(), 2, ',', '.') . ' €',
|
||
'total_price' => number_format((float)$sale->getTotalPrice(), 2, ',', '.') . ' €',
|
||
];
|
||
}
|
||
|
||
$title = 'Inventur-Verkäufe';
|
||
if ($dateFrom !== null || $dateTo !== null) {
|
||
$title .= ' (' . ($dateFrom ?? 'Anfang') . ' – ' . ($dateTo ?? 'Ende') . ')';
|
||
}
|
||
|
||
return [
|
||
'title' => $title,
|
||
'summary' => [
|
||
'total_sales' => count($sales),
|
||
'total_revenue' => number_format($totalRevenue, 2, ',', '.') . ' €',
|
||
],
|
||
'rows' => $rows,
|
||
'headers' => ['Datum', 'Artikel', 'Menge', 'Stückpreis', 'Gesamtsumme'],
|
||
];
|
||
}
|
||
}
|