add refund logic and fix books inventory logic
This commit is contained in:
@@ -76,9 +76,14 @@ class InventoryController extends BaseController
|
||||
$selectedSem = $selectedSemRaw === null ? (string) $this->semester : trim((string) $selectedSemRaw);
|
||||
|
||||
$builder = $this->itemModel->where('type', $type);
|
||||
$this->applyInventoryMovementPeriodFilter($builder, 'inventory_items', $selectedYear, $selectedSem);
|
||||
if ($type !== 'book') {
|
||||
$this->applyInventoryMovementPeriodFilter($builder, 'inventory_items', $selectedYear, $selectedSem);
|
||||
}
|
||||
|
||||
$items = $builder->orderBy('name', 'ASC')->findAll();
|
||||
if ($type === 'book') {
|
||||
$items = $this->attachBookYearRows($items, $selectedYear);
|
||||
}
|
||||
$categories = $this->catModel->optionsForType($type);
|
||||
|
||||
// NEW: build UpdatedBy name map
|
||||
@@ -106,6 +111,9 @@ class InventoryController extends BaseController
|
||||
|
||||
// Load categories for THIS item type
|
||||
$categories = $this->catModel->optionsForType($item['type']);
|
||||
if (($item['type'] ?? '') === 'book') {
|
||||
$item = $this->withBookYearData($item);
|
||||
}
|
||||
|
||||
return view($this->viewForForm($item['type']), [
|
||||
'type' => $item['type'],
|
||||
@@ -135,6 +143,9 @@ class InventoryController extends BaseController
|
||||
if (!$this->itemModel->update($id, $data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode(', ', $this->itemModel->errors()));
|
||||
}
|
||||
if (($item['type'] ?? '') === 'book') {
|
||||
$this->saveBookClassAssignments($id);
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('inventory/' . $item['type']))->with('success', 'Item updated.');
|
||||
}
|
||||
@@ -148,6 +159,15 @@ class InventoryController extends BaseController
|
||||
$itemId = $this->itemModel->insert($itemData, true);
|
||||
if ($itemId) {
|
||||
$initialQty = (int) ($itemData['quantity'] ?? 0);
|
||||
if (($itemData['type'] ?? '') === 'book') {
|
||||
$this->saveBookClassAssignments((int) $itemId);
|
||||
if ($initialQty !== 0) {
|
||||
$this->recordMovement($itemId, $initialQty, 'initial', 'Initial stock');
|
||||
} else {
|
||||
$this->recalcQuantity($itemId);
|
||||
}
|
||||
return redirect()->to(site_url("inventory/{$itemData['type']}"))->with('success', 'Item added.');
|
||||
}
|
||||
if ($initialQty !== 0) {
|
||||
$this->recordMovement($itemId, $initialQty, 'initial', 'Initial stock');
|
||||
} else {
|
||||
@@ -341,6 +361,13 @@ class InventoryController extends BaseController
|
||||
if ($type === 'book') {
|
||||
$base['isbn'] = $this->post('isbn');
|
||||
$base['edition'] = $this->post('edition');
|
||||
$base['author'] = $this->post('author');
|
||||
$base['is_active'] = 1;
|
||||
$isbn = preg_replace('/[^0-9X]/i', '', strtoupper((string) $base['isbn']));
|
||||
$edition = strtolower(trim((string) $base['edition']));
|
||||
$sku = strtolower(trim((string) $base['sku']));
|
||||
$base['isbn_edition_key'] = $isbn !== '' ? $isbn . '|' . $edition : null;
|
||||
$base['sku_normalized'] = $sku !== '' ? $sku : null;
|
||||
} else {
|
||||
$base['isbn'] = $base['edition'] = null;
|
||||
}
|
||||
@@ -348,6 +375,191 @@ class InventoryController extends BaseController
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function withBookYearData(array $item): array
|
||||
{
|
||||
$itemYear = $this->bookItemYear((int) $item['id'], false);
|
||||
if ($itemYear !== null) {
|
||||
$item['charge_price'] = number_format(((int) ($itemYear['charge_price_cents'] ?? 0)) / 100, 2, '.', '');
|
||||
$item['price_confirmed'] = (int) ($itemYear['price_confirmed'] ?? 0);
|
||||
$item['item_year_id'] = (int) $itemYear['id'];
|
||||
$item['opening_quantity'] = (int) ($itemYear['opening_quantity'] ?? 0);
|
||||
}
|
||||
$item['classes'] = array_map(
|
||||
'intval',
|
||||
array_column(
|
||||
$this->db->table('inventory_book_class_assignments')
|
||||
->select('class_number')
|
||||
->where('inventory_item_id', (int) $item['id'])
|
||||
->where('school_year', $this->schoolYear)
|
||||
->orderBy('class_number', 'ASC')
|
||||
->get()->getResultArray(),
|
||||
'class_number'
|
||||
)
|
||||
);
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function attachBookYearRows(array $items, string $schoolYear): array
|
||||
{
|
||||
$ids = array_values(array_filter(array_map(static fn (array $item): int => (int) ($item['id'] ?? 0), $items)));
|
||||
if ($ids === []) {
|
||||
return $items;
|
||||
}
|
||||
$rows = $this->db->table('inventory_item_years')
|
||||
->whereIn('inventory_item_id', $ids)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()->getResultArray();
|
||||
$byItem = [];
|
||||
foreach ($rows as $row) {
|
||||
$byItem[(int) $row['inventory_item_id']] = $row;
|
||||
}
|
||||
foreach ($items as &$item) {
|
||||
$row = $byItem[(int) ($item['id'] ?? 0)] ?? null;
|
||||
if ($row !== null) {
|
||||
$item['charge_price_cents'] = (int) ($row['charge_price_cents'] ?? 0);
|
||||
$item['price_confirmed'] = (int) ($row['price_confirmed'] ?? 0);
|
||||
$item['opening_quantity'] = (int) ($row['opening_quantity'] ?? 0);
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function bookPrices()
|
||||
{
|
||||
$books = $this->itemModel
|
||||
->where('type', 'book')
|
||||
->orderBy('name', 'ASC')
|
||||
->findAll();
|
||||
$books = $this->attachBookYearRows($books, (string) $this->schoolYear);
|
||||
|
||||
return view('inventory/book/prices', [
|
||||
'books' => $books,
|
||||
'categories' => $this->catModel->optionsForType('book'),
|
||||
'schoolYear' => $this->schoolYear,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateBookPrices()
|
||||
{
|
||||
$prices = (array) $this->request->getPost('prices');
|
||||
$confirmed = (array) $this->request->getPost('confirmed');
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', array_keys($prices)))));
|
||||
|
||||
if ($ids === []) {
|
||||
return redirect()->back()->with('warning', 'No book prices were submitted.');
|
||||
}
|
||||
|
||||
$books = $this->itemModel
|
||||
->where('type', 'book')
|
||||
->whereIn('id', $ids)
|
||||
->findAll();
|
||||
$booksById = [];
|
||||
foreach ($books as $book) {
|
||||
$booksById[(int) $book['id']] = $book;
|
||||
}
|
||||
|
||||
$year = $this->currentSchoolYearRow();
|
||||
$updated = 0;
|
||||
|
||||
try {
|
||||
$this->db->transStart();
|
||||
foreach ($ids as $id) {
|
||||
if (! isset($booksById[$id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isConfirmed = isset($confirmed[$id]) && (string) $confirmed[$id] === '1';
|
||||
$priceCents = $this->moneyValueToCents($prices[$id] ?? '');
|
||||
if ($isConfirmed && $priceCents <= 0) {
|
||||
throw new \InvalidArgumentException('Confirmed book prices must be greater than $0.00.');
|
||||
}
|
||||
|
||||
$itemYear = $this->bookItemYear($id, false);
|
||||
$payload = [
|
||||
'inventory_item_id' => $id,
|
||||
'school_year_id' => (int) ($year['id'] ?? 0) ?: null,
|
||||
'school_year' => $this->schoolYear,
|
||||
'charge_price_cents' => $priceCents,
|
||||
'currency' => 'USD',
|
||||
'price_confirmed' => $isConfirmed ? 1 : 0,
|
||||
'status' => 'open',
|
||||
'updated_by' => $this->currentUserId(),
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
if ($itemYear === null) {
|
||||
$payload['opening_quantity'] = (int) ($booksById[$id]['quantity'] ?? 0);
|
||||
$payload['created_by'] = $this->currentUserId();
|
||||
$payload['created_at'] = utc_now();
|
||||
$this->db->table('inventory_item_years')->insert($payload);
|
||||
} else {
|
||||
$this->db->table('inventory_item_years')->where('id', (int) $itemYear['id'])->update($payload);
|
||||
}
|
||||
$updated++;
|
||||
}
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
throw new \RuntimeException('Book prices could not be saved.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('inventory/book-prices'))->with('success', 'Saved prices for ' . $updated . ' book(s).');
|
||||
}
|
||||
|
||||
private function saveBookClassAssignments(int $itemId): void
|
||||
{
|
||||
$classes = array_values(array_unique(array_filter(
|
||||
array_map('intval', (array) $this->request->getPost('classes')),
|
||||
static fn (int $class): bool => $class >= 1 && $class <= 13
|
||||
)));
|
||||
$this->db->table('inventory_book_class_assignments')
|
||||
->where('inventory_item_id', $itemId)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->delete();
|
||||
foreach ($classes as $classNumber) {
|
||||
$this->db->table('inventory_book_class_assignments')->insert([
|
||||
'inventory_item_id' => $itemId,
|
||||
'school_year' => $this->schoolYear,
|
||||
'class_number' => $classNumber,
|
||||
'created_at' => utc_now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function moneyValueToCents($value): int
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return 0;
|
||||
}
|
||||
if (! preg_match('/^\d+(?:\.\d{1,2})?$/', $raw)) {
|
||||
throw new \InvalidArgumentException('Book charge price must be a valid dollar amount with at most two decimals.');
|
||||
}
|
||||
return (int) round(((float) $raw) * 100);
|
||||
}
|
||||
|
||||
private function currentSchoolYearRow(): ?array
|
||||
{
|
||||
return $this->db->table('school_years')
|
||||
->where('name', $this->schoolYear)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
private function bookItemYear(int $itemId, bool $forUpdate = false): ?array
|
||||
{
|
||||
$sql = 'SELECT * FROM inventory_item_years WHERE inventory_item_id = ? AND school_year = ? LIMIT 1';
|
||||
if ($forUpdate) {
|
||||
$sql .= ' FOR UPDATE';
|
||||
}
|
||||
return $this->db->query($sql, [$itemId, $this->schoolYear])->getRowArray();
|
||||
}
|
||||
|
||||
public function auditClassroomForm(int $itemId)
|
||||
{
|
||||
$item = $this->itemModel->find($itemId);
|
||||
@@ -742,12 +954,17 @@ class InventoryController extends BaseController
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('inventory_items i')
|
||||
->select('i.id,i.name,i.isbn,i.edition,i.quantity,i.category_id,i.type,
|
||||
iy.id AS item_year_id, iy.charge_price_cents, iy.price_confirmed,
|
||||
c.name AS category_name, c.grade_min, c.grade_max')
|
||||
->join('inventory_categories c', 'c.id = i.category_id', 'left')
|
||||
->join('inventory_item_years iy', 'iy.inventory_item_id = i.id AND iy.school_year = ' . $db->escape($this->schoolYear), 'left')
|
||||
->join('inventory_book_class_assignments bca', 'bca.inventory_item_id = i.id AND bca.school_year = ' . $db->escape($this->schoolYear), 'left')
|
||||
->where('i.type', 'book');
|
||||
|
||||
if (!empty($classID)) {
|
||||
$builder->groupStart()
|
||||
->where('bca.class_number', (int) $classID)
|
||||
->orGroupStart()
|
||||
// Case A: both bounds set and classID between [min..max]
|
||||
->groupStart()
|
||||
->where('c.grade_min IS NOT NULL', null, false)
|
||||
@@ -769,10 +986,11 @@ class InventoryController extends BaseController
|
||||
->where('c.grade_min', null) // IS NULL
|
||||
->where('c.grade_max', (int)$classID) // =
|
||||
->groupEnd()
|
||||
->groupEnd()
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('i.name', 'ASC')->get()->getResultArray();
|
||||
$rows = $builder->groupBy('i.id')->orderBy('i.name', 'ASC')->get()->getResultArray();
|
||||
|
||||
// Build CATEGORY groups keyed by category_id
|
||||
$labelFor = static function (?array $r): string {
|
||||
@@ -803,6 +1021,8 @@ class InventoryController extends BaseController
|
||||
'id' => (int)$r['id'],
|
||||
'display' => $display,
|
||||
'quantity' => (int)($r['quantity'] ?? 0),
|
||||
'charge_price_cents' => (int)($r['charge_price_cents'] ?? 0),
|
||||
'price_confirmed' => (int)($r['price_confirmed'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -819,16 +1039,16 @@ class InventoryController extends BaseController
|
||||
// 7) History map: how many of the selected book each student already received this school year
|
||||
$already = [];
|
||||
if ($itemId && $classSectionId) {
|
||||
$rowsHist = $this->movModel
|
||||
->select('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty')
|
||||
->where([
|
||||
'item_id' => $itemId,
|
||||
'movement_type' => 'distribution',
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => $ctx['schoolYear'],
|
||||
])
|
||||
$itemYear = $this->bookItemYear($itemId, false);
|
||||
$rowsHist = $this->db->table('student_book_issues')
|
||||
->select('student_id, SUM(quantity) AS qty')
|
||||
->where('inventory_item_id', $itemId)
|
||||
->where('inventory_item_year_id', (int) ($itemYear['id'] ?? 0))
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $ctx['schoolYear'])
|
||||
->where('status', 'issued')
|
||||
->groupBy('student_id')
|
||||
->findAll();
|
||||
->get()->getResultArray();
|
||||
|
||||
foreach ($rowsHist as $r) {
|
||||
$sid = (int) ($r['student_id'] ?? 0);
|
||||
@@ -838,11 +1058,16 @@ class InventoryController extends BaseController
|
||||
|
||||
// 8) On-hand for the selected book (to show live available stock)
|
||||
$onHand = 0;
|
||||
$unitChargeCents = 0;
|
||||
$priceConfirmed = 0;
|
||||
if ($itemId) {
|
||||
$book = $this->itemModel->find($itemId);
|
||||
if ($book && ($book['type'] ?? '') === 'book') {
|
||||
$onHand = (int) ($book['quantity'] ?? 0);
|
||||
}
|
||||
$itemYear = $this->bookItemYear($itemId, false);
|
||||
$unitChargeCents = (int) ($itemYear['charge_price_cents'] ?? 0);
|
||||
$priceConfirmed = (int) ($itemYear['price_confirmed'] ?? 0);
|
||||
}
|
||||
|
||||
// 9) Render view
|
||||
@@ -858,6 +1083,8 @@ class InventoryController extends BaseController
|
||||
'students' => $students,
|
||||
'already' => $already,
|
||||
'onHand' => $onHand,
|
||||
'unitChargeCents' => $unitChargeCents,
|
||||
'priceConfirmed' => $priceConfirmed,
|
||||
|
||||
'schoolYear' => $ctx['schoolYear'],
|
||||
'semester' => $ctx['semester'],
|
||||
@@ -910,50 +1137,34 @@ class InventoryController extends BaseController
|
||||
$selectedIds = array_values(array_unique(array_map('intval', $selectedIds)));
|
||||
$selectedIds = array_values(array_filter($selectedIds, fn($sid) => isset($validIds[$sid])));
|
||||
|
||||
// Already distributed this year for this class/book
|
||||
$already = [];
|
||||
$rows = $this->movModel
|
||||
->select('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty')
|
||||
->where([
|
||||
'item_id' => $itemId,
|
||||
'movement_type' => 'distribution',
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => $ctx['schoolYear'],
|
||||
])->groupBy('student_id')->findAll();
|
||||
foreach ($rows as $r) {
|
||||
$sid = (int)($r['student_id'] ?? 0);
|
||||
if ($sid) $already[$sid] = (int)$r['qty'];
|
||||
try {
|
||||
$year = $this->currentSchoolYearRow();
|
||||
$result = (new \App\Services\StudentBookIssueService($this->db))->distributeBatch(
|
||||
$itemId,
|
||||
$selectedIds,
|
||||
$classSectionId,
|
||||
(int) ($year['id'] ?? 0),
|
||||
(string) $ctx['schoolYear'],
|
||||
$this->currentUserId(),
|
||||
$note,
|
||||
null,
|
||||
(string) $this->request->getPost('idempotency_key')
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()
|
||||
->to(site_url('inventory/books/distribute?class_section_id=' . $classSectionId . '&item_id=' . $itemId))
|
||||
->withInput()
|
||||
->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
// Only assign to students who don't already have it
|
||||
$toGive = [];
|
||||
foreach ($selectedIds as $sid) {
|
||||
if (($already[$sid] ?? 0) < 1) $toGive[] = $sid;
|
||||
}
|
||||
|
||||
// Stock check
|
||||
$needed = count($toGive);
|
||||
$onHand = (int)$book['quantity'];
|
||||
if ($needed > $onHand) {
|
||||
return redirect()->back()->withInput()->with('error', 'Not enough stock: need ' . $needed . ', on hand ' . $onHand . '.');
|
||||
}
|
||||
|
||||
// Record one movement per student
|
||||
$okCount = 0;
|
||||
foreach ($toGive as $sid) {
|
||||
$ok = $this->recordMovement($itemId, -1, 'distribution', 'Teacher distribution', $note, $classSectionId, $sid);
|
||||
if ($ok) $okCount++;
|
||||
}
|
||||
|
||||
if ($okCount === 0) {
|
||||
if ((int) ($result['issued'] ?? 0) === 0) {
|
||||
return redirect()
|
||||
->to(site_url('inventory/books/distribute?class_section_id=' . $classSectionId . '&item_id=' . $itemId))
|
||||
->with('warning', 'No changes (everyone selected already has this book).');
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->to(site_url('inventory/books/distribute?class_section_id=' . $classSectionId . '&item_id=' . $itemId))
|
||||
->with('success', 'Distributed to ' . $okCount . ' student(s).');
|
||||
->with('success', 'Distributed to ' . (int) $result['issued'] . ' student(s).');
|
||||
}
|
||||
|
||||
|
||||
@@ -1105,7 +1316,9 @@ class InventoryController extends BaseController
|
||||
|
||||
public function create(string $type = 'classroom')
|
||||
{
|
||||
$type = $this->normalizeType($type);
|
||||
$categories = $this->catModel->optionsForType($type);
|
||||
|
||||
return view("inventory/{$type}/form", [
|
||||
'type' => $type,
|
||||
'item' => [], // important for create
|
||||
@@ -1347,6 +1560,10 @@ class InventoryController extends BaseController
|
||||
$userId = (int) (session('user_id') ?? 0);
|
||||
$itemId = (int) $this->request->getPost('item_id');
|
||||
$movementType = (string) $this->request->getPost('movement_type');
|
||||
if ($movementType === 'distribution') {
|
||||
return redirect()->back()->withInput()->with('status', 'error')
|
||||
->with('message', 'Student book distributions must be created from the book distribution workflow.');
|
||||
}
|
||||
|
||||
$item = $this->itemModel->find($itemId);
|
||||
if (!$item) {
|
||||
@@ -1404,6 +1621,10 @@ class InventoryController extends BaseController
|
||||
return redirect()->to(site_url('inventory/movements'))
|
||||
->with('status', 'error')->with('message', 'Movement not found.');
|
||||
}
|
||||
if ($this->isProtectedMovement($movement)) {
|
||||
return redirect()->to(site_url('inventory/movements'))
|
||||
->with('status', 'error')->with('message', 'Issue-backed or distribution movements are read-only. Use correction reversal instead.');
|
||||
}
|
||||
|
||||
// enrich for display (names)
|
||||
$movement = $this->hydrateNames($movement);
|
||||
@@ -1449,8 +1670,16 @@ class InventoryController extends BaseController
|
||||
return redirect()->to(site_url('inventory/movements'))
|
||||
->with('status', 'error')->with('message', 'Movement not found.');
|
||||
}
|
||||
if ($this->isProtectedMovement($existing)) {
|
||||
return redirect()->to(site_url('inventory/movements'))
|
||||
->with('status', 'error')->with('message', 'Issue-backed or distribution movements are read-only. Use correction reversal instead.');
|
||||
}
|
||||
|
||||
$movementType = (string) $this->request->getPost('movement_type');
|
||||
if ($movementType === 'distribution') {
|
||||
return redirect()->back()->withInput()->with('status', 'error')
|
||||
->with('message', 'Student book distributions must be created from the book distribution workflow.');
|
||||
}
|
||||
$qtyChange = $this->normalizeMovementQty($movementType, (int) $this->request->getPost('qty_change'));
|
||||
|
||||
$itemId = (int) ($existing['item_id'] ?? 0);
|
||||
@@ -1504,10 +1733,13 @@ class InventoryController extends BaseController
|
||||
{
|
||||
// Optional: authorization checks here
|
||||
log_message('info', 'Inventory: delete one attempt', ['id' => $id, 'user' => (int)(session('user_id') ?? 0)]);
|
||||
$movement = $this->db->table('inventory_movements')->select('id, item_id')->where('id', $id)->get()->getRowArray();
|
||||
$movement = $this->db->table('inventory_movements')->select('*')->where('id', $id)->get()->getRowArray();
|
||||
if (!$movement) {
|
||||
return redirect()->back()->with('status', 'error')->with('message', 'Movement not found.');
|
||||
}
|
||||
if ($this->isProtectedMovement($movement)) {
|
||||
return redirect()->back()->with('status', 'error')->with('message', 'Issue-backed or distribution movements are read-only. Use correction reversal instead.');
|
||||
}
|
||||
|
||||
$ok = $this->db->table('inventory_movements')->where('id', $id)->delete();
|
||||
log_message('info', 'Inventory: delete one result', ['id' => $id, 'ok' => (bool)$ok, 'affected' => $this->db->affectedRows()]);
|
||||
@@ -1577,6 +1809,16 @@ class InventoryController extends BaseController
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isProtectedMovement(array $movement): bool
|
||||
{
|
||||
$type = (string) ($movement['movement_type'] ?? '');
|
||||
$sourceType = (string) ($movement['source_type'] ?? '');
|
||||
return $type === 'distribution'
|
||||
|| in_array($sourceType, ['student_book_issue', 'student_book_issue_reversal'], true)
|
||||
|| (int) ($movement['source_id'] ?? 0) > 0
|
||||
|| (int) ($movement['reversal_of_movement_id'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
/** Hydrate display names for a row (used in edit) */
|
||||
private function hydrateNames(array $m): array
|
||||
{
|
||||
@@ -1682,13 +1924,18 @@ class InventoryController extends BaseController
|
||||
}
|
||||
|
||||
log_message('info', 'Inventory: bulk delete attempt', ['count' => count($ids), 'ids' => $ids, 'user' => (int)(session('user_id') ?? 0)]);
|
||||
$itemIds = $this->db->table('inventory_movements')
|
||||
->select('item_id')
|
||||
$movements = $this->db->table('inventory_movements')
|
||||
->select('*')
|
||||
->whereIn('id', $ids)
|
||||
->get()->getResultArray();
|
||||
foreach ($movements as $movement) {
|
||||
if ($this->isProtectedMovement($movement)) {
|
||||
return redirect()->back()->with('status','error')->with('message','Bulk delete contains issue-backed or distribution movement #' . (int) $movement['id'] . '. Use correction reversal instead.');
|
||||
}
|
||||
}
|
||||
$itemIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn($r) => (int) ($r['item_id'] ?? 0),
|
||||
$itemIds
|
||||
$movements
|
||||
))));
|
||||
|
||||
$ok = $this->db->table('inventory_movements')->whereIn('id', $ids)->delete();
|
||||
|
||||
Reference in New Issue
Block a user