mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-10-02 23:55:19 -04:00
feat(items): optimize search, attribute filtering, and sort for items view (#4652) BREAKING CHANGE: none ## Search & Query Optimization - Split item search into two-phase query: Phase A resolves qualifying IDs, Phase B joins scoped display tables for better performance and readability - Sanitize `definition_ids` via `array_map` to prevent injection vulnerabilities - Introduce subquery for SUM aggregation to prevent over-counting across joins - Add validation requiring both start and end dates before applying date range filter ## Attribute Search (fixes #2919, #2722) - Add `SHOW_IN_SEARCH` flag (value 8) to Attribute model to separate searchability from table visibility - Add `parse_attribute_search()` to parse syntax like `color:blue AND size:large` - Add `applyNamedAttributeSearch()` supporting decimal and date types with locale-aware parsing - Support AND/OR logic for multi-attribute queries ## Sorting - Add `get_attribute_sort_definition_id()` to detect attribute column sorting - Join attribute tables dynamically when sorting by attribute columns - Use `MAX()` for consistent results when sorting by attribute values - Replace static sort column list with dynamic headers via `itemSortColumns()` - Add `sanitizeSortColumnAttribute()` to validate attribute definition IDs as sort columns ## Tax & Data Row - Streamline tax computation in `getItemDataRow()` ## Low Inventory Filter - Require valid `stock_location_id` before applying low inventory filter - Add conditional logic to sort by sum of quantities across all locations when `stock_location_id` is invalid ## Localization - Add `show_in_search` / `show_in_search_visibility` strings to all language files - Translated: de-DE, es-ES, fr, it; English placeholder for remaining locales - Unify single-quote style across all attribute language files ## Refactoring & Style - Adopt camelCase naming throughout (variables, helpers, methods) - Replace `sanitizeSortColumnAttribute` with reusable `sanitizeSortColumn` from `Secure_Controller` - Simplify column key extraction using `array_key_first` - Apply PSR-12 formatting ## Tests - Add tests for tax computation, quantity aggregation (single- and multi-location), named attribute search, free-text parsing, and date/decimal type handling - Add `ensureStockLocation` helper to auto-create missing stock locations in tests - Refactor tests to handle config cache issues Co-authored-by: Ollama <ollama@steganos.dev>
134 lines
4.4 KiB
PHP
134 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Support;
|
|
|
|
use App\Models\Attribute;
|
|
use App\Models\Inventory;
|
|
use App\Models\Item;
|
|
use App\Models\Item_quantity;
|
|
|
|
trait ItemSearchFixtureTrait
|
|
{
|
|
protected function createSearchableItem(array $overrides = []): int
|
|
{
|
|
$itemData = array_merge([
|
|
'item_id' => null,
|
|
'name' => 'Searchable Item ' . uniqid(),
|
|
'category' => 'Test Category',
|
|
'cost_price' => 1.00,
|
|
'unit_price' => 5.00,
|
|
'reorder_level' => 0,
|
|
'item_number' => 'SEARCH-' . uniqid(),
|
|
'allow_alt_description' => 0,
|
|
'is_serialized' => 0,
|
|
'item_type' => ITEM,
|
|
'deleted' => 0,
|
|
], $overrides);
|
|
|
|
$itemModel = model(Item::class);
|
|
$itemModel->save_value($itemData);
|
|
|
|
$itemId = (int) $itemData['item_id'];
|
|
|
|
$this->addInventoryRecord($itemId);
|
|
|
|
return $itemId;
|
|
}
|
|
|
|
protected function addInventoryRecord(int $itemId, int $locationId = 1): void
|
|
{
|
|
$inventoryModel = model(Inventory::class);
|
|
$inventoryModel->insert([
|
|
'trans_items' => $itemId,
|
|
'trans_user' => 1,
|
|
'trans_comment' => 'Test fixture',
|
|
'trans_inventory' => 0,
|
|
'trans_location' => $locationId,
|
|
]);
|
|
}
|
|
|
|
protected function ensureStockLocation(int $locationId): void
|
|
{
|
|
$db = db_connect();
|
|
$exists = $db->table('stock_locations')->where('location_id', $locationId)->get()->getRow();
|
|
if ($exists === null) {
|
|
$db->query(
|
|
'INSERT INTO ' . $db->prefixTable('stock_locations') . ' (location_id, location_name, deleted) VALUES (?, ?, 0)',
|
|
[$locationId, 'Test Location ' . $locationId]
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function addItemQuantity(int $itemId, int $locationId, float $quantity): void
|
|
{
|
|
$this->ensureStockLocation($locationId);
|
|
|
|
$itemQuantityModel = model(Item_quantity::class);
|
|
$itemQuantityModel->save_value(
|
|
[
|
|
'item_id' => $itemId,
|
|
'location_id' => $locationId,
|
|
'quantity' => $quantity,
|
|
],
|
|
$itemId,
|
|
$locationId
|
|
);
|
|
}
|
|
|
|
protected function defaultSearchFilters(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'start_date' => '2000-01-01',
|
|
'end_date' => '2100-01-01',
|
|
'stock_location_id' => -1,
|
|
'empty_upc' => false,
|
|
'low_inventory' => false,
|
|
'is_serialized' => false,
|
|
'no_description' => false,
|
|
'search_custom' => false,
|
|
'is_deleted' => false,
|
|
'temporary' => false,
|
|
'definition_ids' => [],
|
|
], $overrides);
|
|
}
|
|
|
|
protected function createAttributeDefinition(string $name, string $type = TEXT, int $flags = 1): int
|
|
{
|
|
$db = db_connect();
|
|
$db->table('attribute_definitions')->insert([
|
|
'definition_name' => $name,
|
|
'definition_type' => $type,
|
|
'definition_flags' => $flags,
|
|
'deleted' => 0,
|
|
]);
|
|
|
|
return (int) $db->insertID();
|
|
}
|
|
|
|
protected function linkAttributeValue(int $itemId, int $definitionId, string $value): void
|
|
{
|
|
$db = db_connect();
|
|
|
|
// attribute_value is unique: reuse the existing attribute_id if this value already exists
|
|
$existing = $db->table('attribute_values')->select('attribute_id')->where('attribute_value', $value)->get()->getRow();
|
|
if ($existing !== null) {
|
|
$attributeId = (int) $existing->attribute_id;
|
|
} else {
|
|
$db->table('attribute_values')->insert(['attribute_value' => $value]);
|
|
$attributeId = (int) $db->insertID();
|
|
}
|
|
|
|
$db->table('attribute_links')->insert([
|
|
'definition_id' => $definitionId,
|
|
'attribute_id' => $attributeId,
|
|
'item_id' => $itemId,
|
|
]);
|
|
}
|
|
|
|
protected function linkTypedAttributeValue(int $itemId, int $definitionId, string $value, string $definitionType): void
|
|
{
|
|
$attributeModel = model(Attribute::class);
|
|
$attributeModel->saveAttributeValue($value, $definitionId, $itemId, false, $definitionType);
|
|
}
|
|
}
|