fixed stocktake and warehouselocation
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
class WarehouseLocationModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $description;
|
||||
public ?string $description = null;
|
||||
public int $assignedTo;
|
||||
public int $createBy;
|
||||
public int $create;
|
||||
|
||||
@@ -214,7 +214,8 @@ class WarehouseStocktakeController extends TTCrud {
|
||||
|
||||
// Get items via direct SQL to avoid any ORM issues
|
||||
$db = FronkDB::singleton();
|
||||
$result = $db->query("SELECT si.*, a.articleNumber, a.title as articleTitle, w.name as scannedByName
|
||||
$result = $db->query("SELECT si.*, a.articleNumber, a.title as articleTitle, a.cheapestPurchasePrice, w.name as scannedByName,
|
||||
CASE WHEN si.overwrittenById IS NOT NULL THEN 1 ELSE 0 END as isOverwritten
|
||||
FROM WarehouseStocktakeItem si
|
||||
LEFT JOIN WarehouseArticle a ON si.articleId = a.id
|
||||
LEFT JOIN Worker w ON si.scannedBy = w.id
|
||||
@@ -222,18 +223,34 @@ class WarehouseStocktakeController extends TTCrud {
|
||||
ORDER BY si.`create` DESC");
|
||||
|
||||
$formattedItems = [];
|
||||
$totalValue = 0;
|
||||
$totalQuantity = 0;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$unitPrice = $row['cheapestPurchasePrice'] ? (float)$row['cheapestPurchasePrice'] : 0;
|
||||
$quantity = (float)$row['countedQuantity'];
|
||||
$lineTotal = $unitPrice * $quantity;
|
||||
$isOverwritten = (bool)$row['isOverwritten'];
|
||||
|
||||
// Only count non-overwritten items in totals
|
||||
if (!$isOverwritten) {
|
||||
$totalValue += $lineTotal;
|
||||
$totalQuantity += $quantity;
|
||||
}
|
||||
|
||||
$formattedItems[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'articleId' => (int)$row['articleId'],
|
||||
'articleNumber' => $row['articleNumber'] ?? '',
|
||||
'articleTitle' => $row['articleTitle'] ?? 'Unbekannt',
|
||||
'countedQuantity' => (float)$row['countedQuantity'],
|
||||
'countedQuantity' => $quantity,
|
||||
'unitPrice' => $unitPrice,
|
||||
'lineTotal' => $lineTotal,
|
||||
'rack' => $row['rack'],
|
||||
'shelf' => $row['shelf'],
|
||||
'note' => $row['note'],
|
||||
'scannedAt' => $row['scannedAt'] ? date('d.m.Y H:i:s', $row['scannedAt']) : null,
|
||||
'scannedBy' => $row['scannedByName'],
|
||||
'isOverwritten' => $isOverwritten,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -251,6 +268,10 @@ class WarehouseStocktakeController extends TTCrud {
|
||||
'startedAt' => $stocktake->startedAt ? date('d.m.Y H:i', $stocktake->startedAt) : null,
|
||||
],
|
||||
'items' => $formattedItems,
|
||||
'summary' => [
|
||||
'totalValue' => $totalValue,
|
||||
'totalQuantity' => $totalQuantity,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -347,25 +368,53 @@ class WarehouseStocktakeController extends TTCrud {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = WarehouseStocktakeItemModel::getAll(['stocktakeId' => $id]);
|
||||
$rows = [];
|
||||
// Get items via direct SQL to include price and overwritten status
|
||||
$db = FronkDB::singleton();
|
||||
$result = $db->query("SELECT si.*, a.articleNumber, a.title as articleTitle, a.cheapestPurchasePrice, w.name as scannedByName
|
||||
FROM WarehouseStocktakeItem si
|
||||
LEFT JOIN WarehouseArticle a ON si.articleId = a.id
|
||||
LEFT JOIN Worker w ON si.scannedBy = w.id
|
||||
WHERE si.stocktakeId = {$id}
|
||||
ORDER BY si.`create` ASC");
|
||||
|
||||
foreach ($items as $item) {
|
||||
$article = $item->getArticle();
|
||||
$scannedBy = $item->getScannedByUser();
|
||||
$rows = [];
|
||||
$totalSum = 0;
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$unitPrice = $row['cheapestPurchasePrice'] ? (float)$row['cheapestPurchasePrice'] : 0;
|
||||
$quantity = (float)$row['countedQuantity'];
|
||||
$lineTotal = $unitPrice * $quantity;
|
||||
$isOverwritten = !empty($row['overwrittenById']);
|
||||
|
||||
// Skip overwritten items in calculation but show them
|
||||
if (!$isOverwritten) {
|
||||
$totalSum += $lineTotal;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'Artikel-Nr.' => $article ? $article->articleNumber : '',
|
||||
'Artikel' => $article ? $article->title : 'Unbekannt',
|
||||
'Menge' => $item->countedQuantity,
|
||||
'Regal' => $item->rack ?? '',
|
||||
'Fach' => $item->shelf ?? '',
|
||||
'Notiz' => $item->note ?? '',
|
||||
'Gescannt am' => $item->scannedAt ? date('d.m.Y H:i', $item->scannedAt) : '',
|
||||
'Gescannt von' => $scannedBy ? $scannedBy->name : '',
|
||||
'Artikel Titel' => $row['articleTitle'] ?? 'Unbekannt',
|
||||
'Artikel Nummer' => $row['articleNumber'] ?? '',
|
||||
'Einzelpreis' => number_format($unitPrice, 2, ',', '.') . ' €',
|
||||
'Anzahl' => $quantity,
|
||||
'Gesamtsumme' => number_format($lineTotal, 2, ',', '.') . ' €',
|
||||
'Gescannt am' => $row['scannedAt'] ? date('d.m.Y H:i', $row['scannedAt']) : '',
|
||||
'Gescannt von' => $row['scannedByName'] ?? '',
|
||||
'Status' => $isOverwritten ? 'Überschrieben' : '',
|
||||
];
|
||||
}
|
||||
|
||||
// Add summary row
|
||||
$rows[] = [
|
||||
'Artikel Titel' => '',
|
||||
'Artikel Nummer' => '',
|
||||
'Einzelpreis' => '',
|
||||
'Anzahl' => 'SUMME:',
|
||||
'Gesamtsumme' => number_format($totalSum, 2, ',', '.') . ' €',
|
||||
'Gescannt am' => '',
|
||||
'Gescannt von' => '',
|
||||
'Status' => '',
|
||||
];
|
||||
|
||||
$filename = "Inventur_{$stocktake->stocktakeNumber}_" . date('Y-m-d') . ".csv";
|
||||
$csv = Helper::arrayToCsv($rows);
|
||||
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
class WarehouseStocktakeModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public ?string $stocktakeNumber;
|
||||
public ?string $stocktakeNumber = null;
|
||||
public string $title;
|
||||
public ?string $description;
|
||||
public ?string $description = null;
|
||||
public int $warehouseLocationId;
|
||||
public string $status;
|
||||
public ?int $startedAt;
|
||||
public ?int $completedAt;
|
||||
public ?int $startedBy;
|
||||
public ?int $completedBy;
|
||||
public string $status = 'planned';
|
||||
public ?int $startedAt = null;
|
||||
public ?int $completedAt = null;
|
||||
public ?int $startedBy = null;
|
||||
public ?int $completedBy = null;
|
||||
public int $totalItems = 0;
|
||||
public int $totalScannedItems = 0;
|
||||
public ?string $notes;
|
||||
public ?string $notes = null;
|
||||
public int $createBy;
|
||||
public int $create;
|
||||
|
||||
|
||||
@@ -157,9 +157,10 @@ class WarehouseStocktakeItemController extends TTCrud {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse QR code format: WH:articleId:articleNumber
|
||||
// Try to parse QR code format: WA:articleId:articleNumber (Warehouse Article)
|
||||
// Also accept WH: for backwards compatibility
|
||||
$articleId = null;
|
||||
if (preg_match('/^WH:(\d+):/', $code, $matches)) {
|
||||
if (preg_match('/^(?:WA|WH):(\d+):/', $code, $matches)) {
|
||||
$articleId = intval($matches[1]);
|
||||
} else {
|
||||
// Try to find by article number
|
||||
|
||||
@@ -11,6 +11,7 @@ class WarehouseStocktakeItemModel extends TTCrudBaseModel {
|
||||
public ?string $note;
|
||||
public ?int $scannedAt;
|
||||
public ?int $scannedBy;
|
||||
public ?int $overwrittenById;
|
||||
public int $createBy;
|
||||
public int $create;
|
||||
|
||||
@@ -31,8 +32,8 @@ class WarehouseStocktakeItemModel extends TTCrudBaseModel {
|
||||
/**
|
||||
* Get user who scanned this item
|
||||
*/
|
||||
public function getScannedByUser(): ?UserModel {
|
||||
public function getScannedByUser(): ?User {
|
||||
if (!$this->scannedBy) return null;
|
||||
return UserModel::get($this->scannedBy);
|
||||
return UserModel::getOne($this->scannedBy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
|
||||
$articleId = null;
|
||||
|
||||
// Try to parse QR code format: WH:articleId:articleNumber
|
||||
if (preg_match('/^WH:(\d+):/', $code, $matches)) {
|
||||
// Try to parse QR code format: WA:articleId:articleNumber (Warehouse Article)
|
||||
// Also accept WH: for backwards compatibility
|
||||
if (preg_match('/^(?:WA|WH):(\d+):/', $code, $matches)) {
|
||||
$articleId = intval($matches[1]);
|
||||
} else {
|
||||
// Try to find by article number
|
||||
@@ -134,7 +135,7 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
}
|
||||
|
||||
// Get category name
|
||||
$category = WarehouseCategoryModel::get($article->category_id);
|
||||
$category = WarehouseCategory::get($article->category_id);
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
@@ -150,24 +151,35 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search articles by text
|
||||
* Search articles by text with optional category filter
|
||||
*/
|
||||
protected function searchArticlesAction() {
|
||||
$query = $this->request->query;
|
||||
$query = $this->request->query ?? '';
|
||||
$categoryId = intval($this->request->categoryId ?? 0);
|
||||
|
||||
if (!$query || strlen($query) < 2) {
|
||||
$db = FronkDB::singleton();
|
||||
$conditions = ["(isEndOfLife IS NULL OR isEndOfLife = 0)"];
|
||||
|
||||
if ($query && strlen($query) >= 2) {
|
||||
$escapedQuery = $db->escape($query);
|
||||
$conditions[] = "(articleNumber LIKE '%{$escapedQuery}%' OR title LIKE '%{$escapedQuery}%' OR description LIKE '%{$escapedQuery}%')";
|
||||
}
|
||||
|
||||
if ($categoryId > 0) {
|
||||
$conditions[] = "category_id = {$categoryId}";
|
||||
}
|
||||
|
||||
if (count($conditions) === 1 && !$categoryId) {
|
||||
self::returnJson(['success' => true, 'articles' => []]);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = FronkDB::singleton();
|
||||
$escapedQuery = $db->escape($query);
|
||||
|
||||
$result = $db->query("SELECT id, articleNumber, title, unit
|
||||
$whereClause = implode(' AND ', $conditions);
|
||||
$result = $db->query("SELECT id, articleNumber, title, unit, category_id
|
||||
FROM WarehouseArticle
|
||||
WHERE (articleNumber LIKE '%{$escapedQuery}%' OR title LIKE '%{$escapedQuery}%')
|
||||
AND (isEndOfLife IS NULL OR isEndOfLife = 0)
|
||||
LIMIT 20");
|
||||
WHERE {$whereClause}
|
||||
ORDER BY title ASC
|
||||
LIMIT 50");
|
||||
|
||||
$articles = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
@@ -176,12 +188,68 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
'articleNumber' => $row['articleNumber'],
|
||||
'title' => $row['title'],
|
||||
'unit' => $row['unit'] ?? 'Stk.',
|
||||
'categoryId' => intval($row['category_id'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true, 'articles' => $articles]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories for browsing
|
||||
*/
|
||||
protected function getCategoriesAction() {
|
||||
$db = FronkDB::singleton();
|
||||
$res = $db->query("SELECT id, name FROM WarehouseCategory ORDER BY name ASC");
|
||||
|
||||
$categories = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$categories[] = [
|
||||
'id' => intval($row['id']),
|
||||
'name' => $row['name'],
|
||||
];
|
||||
}
|
||||
self::returnJson(['success' => true, 'categories' => $categories]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if article is already scanned in stocktake
|
||||
*/
|
||||
protected function checkAlreadyScannedAction() {
|
||||
$stocktakeId = intval($this->request->stocktakeId);
|
||||
$articleId = intval($this->request->articleId);
|
||||
|
||||
if (!$stocktakeId || !$articleId) {
|
||||
self::returnJson(['success' => false, 'message' => 'Fehlende Parameter']);
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = WarehouseStocktakeItemModel::getFirst([
|
||||
'stocktakeId' => $stocktakeId,
|
||||
'articleId' => $articleId,
|
||||
'overwrittenById' => null
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
$db = FronkDB::singleton();
|
||||
$scannedByResult = $db->query("SELECT name FROM Worker WHERE id = {$existing->scannedBy}");
|
||||
$scannedByRow = $scannedByResult->fetch_assoc();
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
'alreadyScanned' => true,
|
||||
'existingItem' => [
|
||||
'id' => $existing->id,
|
||||
'countedQuantity' => $existing->countedQuantity,
|
||||
'scannedAt' => $existing->scannedAt ? date('d.m.Y H:i', $existing->scannedAt) : null,
|
||||
'scannedBy' => $scannedByRow ? $scannedByRow['name'] : 'Unbekannt',
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
self::returnJson(['success' => true, 'alreadyScanned' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a scanned item
|
||||
*/
|
||||
@@ -194,6 +262,8 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
$rack = $postData['rack'] ?? null;
|
||||
$shelf = $postData['shelf'] ?? null;
|
||||
$note = $postData['note'] ?? null;
|
||||
$overwrite = boolval($postData['overwrite'] ?? false);
|
||||
$overwriteItemId = intval($postData['overwriteItemId'] ?? 0);
|
||||
|
||||
if (!$stocktakeId || !$articleId) {
|
||||
self::returnJson(['success' => false, 'message' => 'Fehlende Parameter']);
|
||||
@@ -224,14 +294,64 @@ class WarehouseStocktakePWAController extends mfBaseController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this article was already scanned in this stocktake
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
// If overwrite mode is enabled, mark existing item as overwritten
|
||||
if ($overwrite && $overwriteItemId) {
|
||||
// Create new entry
|
||||
$db->query("INSERT INTO WarehouseStocktakeItem
|
||||
(stocktakeId, articleId, countedQuantity, rack, shelf, note, scannedAt, scannedBy, createBy, `create`)
|
||||
VALUES ({$stocktakeId}, {$articleId}, {$quantity},
|
||||
" . ($rack ? "'{$db->escape($rack)}'" : "NULL") . ",
|
||||
" . ($shelf ? "'{$db->escape($shelf)}'" : "NULL") . ",
|
||||
" . ($note ? "'{$db->escape($note)}'" : "NULL") . ",
|
||||
" . time() . ", {$this->user->id}, {$this->user->id}, " . time() . ")");
|
||||
|
||||
$itemId = $db->insert_id;
|
||||
|
||||
// Mark old item as overwritten by new item
|
||||
$db->query("UPDATE WarehouseStocktakeItem SET overwrittenById = {$itemId} WHERE id = {$overwriteItemId}");
|
||||
|
||||
$finalQuantity = $quantity;
|
||||
$isOverwrite = true;
|
||||
|
||||
// Log the overwrite
|
||||
WarehouseStocktakeLogModel::log($stocktakeId, 'overwritten', $itemId, [
|
||||
'articleId' => $articleId,
|
||||
'articleNumber' => $article->articleNumber,
|
||||
'articleTitle' => $article->title,
|
||||
'quantity' => $quantity,
|
||||
'overwrittenItemId' => $overwriteItemId,
|
||||
]);
|
||||
|
||||
// Update stocktake progress (don't increase count since we're replacing)
|
||||
$stocktake->updateProgress();
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
'message' => "'{$article->title}' überschrieben ({$quantity} {$article->unit})",
|
||||
'item' => [
|
||||
'id' => $itemId,
|
||||
'articleId' => $articleId,
|
||||
'articleNumber' => $article->articleNumber,
|
||||
'articleTitle' => $article->title,
|
||||
'countedQuantity' => $finalQuantity,
|
||||
'unit' => $article->unit ?? 'Stk.',
|
||||
'rack' => $rack,
|
||||
'shelf' => $shelf,
|
||||
'isOverwrite' => true,
|
||||
]
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this article was already scanned in this stocktake (non-overwritten)
|
||||
$existing = WarehouseStocktakeItemModel::getFirst([
|
||||
'stocktakeId' => $stocktakeId,
|
||||
'articleId' => $articleId
|
||||
'articleId' => $articleId,
|
||||
'overwrittenById' => null
|
||||
]);
|
||||
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
if ($existing) {
|
||||
// Update existing entry - add to quantity
|
||||
$newQuantity = $existing->countedQuantity + $quantity;
|
||||
|
||||
Reference in New Issue
Block a user