feat: Implement fixed sale price functionality for items
- Added a toggle for fixed sale price vs stock value in the item creation form. - Introduced validation to ensure all variants have a price greater than 0 when fixed price mode is selected. - Updated the item model to include a nullable salePrice field, which is used for sales only and does not affect GRN/FIFO/ledger. - Enhanced the GRN page to allow off-PO items and included a refresh button to update the item list without reloading the page. - Updated documentation to reflect changes in item pricing and GRN handling.
This commit is contained in:
@@ -164,6 +164,8 @@ docs/10 C.9): every write below forwards to AuthHex's new `/api/role` functions
|
||||
|
||||
### 2.1 Items
|
||||
> **`itemType` → `stockNature` (2026-07-16).** The Stocked/NonStocked/Service field was renamed so the name `itemType` could be taken by the new Item Type master (§2.7) — an unrelated concept. Items gained `subCategoryId` and `brandId` (both nullable). Items carry **no** item-type reference: the values chosen in the builder are encoded into the client-generated SKU (docs/10 Part C.9).
|
||||
>
|
||||
> **`salePrice` added (nullable, 2026-07-22).** Every item body below carries `salePrice` (`number|null`). It is the **Sales-only** fixed selling price: `null` ⇒ the item is sold at its stock/FIFO value; a value ⇒ fixed price. It never affects GRN/FIFO/ledger. On write it is optional; when supplied it must be `>= 0` (else `400` validation). The item builder's "fixed price / use stock value" toggle is UI-only — the contract is just the nullable field.
|
||||
|
||||
#### `GET /items`
|
||||
Query: `q`, `status` (`Active|Inactive`), `categoryId`, `subCategoryId`, `brandId`, `trackingMode` (`None|Batch|Serial`), + paging.
|
||||
@@ -171,7 +173,8 @@ Query: `q`, `status` (`Active|Inactive`), `categoryId`, `subCategoryId`, `brandI
|
||||
```json
|
||||
{ "items": [ { "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
|
||||
"categoryId": 12, "subCategoryId": 30, "brandId": 2, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"stockNature": "Stocked", "trackingMode": "Batch", "taxClass": "STD", "status": "Active" } ],
|
||||
"stockNature": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
|
||||
"salePrice": 12.5000, "status": "Active" } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
|
||||
```
|
||||
|
||||
@@ -181,7 +184,8 @@ Query: `q`, `status` (`Active|Inactive`), `categoryId`, `subCategoryId`, `brandI
|
||||
"description": "Grade 8.8 zinc-plated hex bolt", "categoryId": 12, "subCategoryId": 30,
|
||||
"brandId": 2, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "stockNature": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
|
||||
"status": "Active", "reorder": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 } ],
|
||||
"salePrice": 12.5000, "status": "Active",
|
||||
"reorder": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 } ],
|
||||
"conversions": [ { "conversionId": 33, "fromUom": 7, "toUom": 1, "factor": 12 } ],
|
||||
"createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-07-01T10:15:00Z" }
|
||||
```
|
||||
@@ -192,14 +196,14 @@ The `sku` is **generated by the client** (it encodes the chosen item-type values
|
||||
```json
|
||||
{ "sku": "ITM-1002", "name": "Steel Nut M8", "description": "Grade 8 zinc-plated hex nut",
|
||||
"categoryId": 12, "subCategoryId": 30, "brandId": 2, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"stockNature": "Stocked", "trackingMode": "None", "taxClass": "STD" }
|
||||
"stockNature": "Stocked", "trackingMode": "None", "taxClass": "STD", "salePrice": 3.2500 }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/items/1002`
|
||||
```json
|
||||
{ "itemId": 1002, "sku": "ITM-1002", "name": "Steel Nut M8", "categoryId": 12,
|
||||
"subCategoryId": 30, "brandId": 2, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "stockNature": "Stocked", "trackingMode": "None", "taxClass": "STD",
|
||||
"status": "Active", "createdAt": "2026-07-07T09:30:00Z" }
|
||||
"salePrice": 3.2500, "status": "Active", "createdAt": "2026-07-07T09:30:00Z" }
|
||||
```
|
||||
`400` → `code: SKU_DUPLICATE` if SKU exists.
|
||||
`422` → `code: CONFIG_DISABLED` if `subCategoryId` is sent while subcategories are disabled, or `brandId` while brands are disabled (§2.8).
|
||||
@@ -530,11 +534,18 @@ Against a PO (lines default from open PO lines) or direct (`poId: null`, by perm
|
||||
{ "poId": 342, "warehouseId": 1,
|
||||
"lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45, "qty": 5000,
|
||||
"unitCost": 12.50, "discountPct": 10, "vatPct": 18, "holdStatus": "OnHold",
|
||||
"batch": { "batchNo": "B-2607", "expiryDate": "2028-07-01" } } ] }
|
||||
"batch": { "batchNo": "B-2607", "expiryDate": "2028-07-01" } },
|
||||
{ "poLineId": null, "itemId": 1050, "uomId": 1, "qty": 20,
|
||||
"unitCost": 8.00, "holdStatus": "Available" } ] }
|
||||
```
|
||||
`discountPct`/`vatPct` optional (default 0, range 0–100). `unitCost` on a **PO line** is an optional
|
||||
override: 0/omitted uses the PO price; a value wins and a variance is recorded (02-SECURITY C.3, revised).
|
||||
On a direct receipt `unitCost` is required.
|
||||
**Off-PO lines (`poLineId: null`) are allowed even on a PO-based GRN** (2026-07-22, FR-GRN-01) — the second
|
||||
line above receives an item that is not on the PO. Such a line behaves exactly like a direct-receipt line:
|
||||
`unitCost` is required, `OVER_RECEIPT_TOLERANCE` does **not** apply (there is no PO qty to check), and no PO
|
||||
line balance is touched. The inline "create new item" UI simply calls `POST /items` (§2.1) first, then adds
|
||||
the returned item as an off-PO line.
|
||||
**201 Created** — status `Draft`. All derived figures are **server-computed**:
|
||||
`netUnitCost = unitCost × (1 − discountPct/100)`, `receivedValue = qty × netUnitCost` (after discount,
|
||||
**before** VAT — this is the stock value), `vatAmount = receivedValue × vatPct/100`,
|
||||
|
||||
Reference in New Issue
Block a user