Add Stock catalog (items/warehouses) and fix session persistence
Stock is scoped to a catalog + cost centers only, per direction that physical GRV/stock-take/recipe transactions belong in an external system (Matrix), not this accounting app. Stock Control/GRN/Variance/ PPV GL accounts are now plain postable accounts rather than locked control accounts. Also fixes session loss on page refresh: auth stores now persist to localStorage, and ProtectedRoute waits for rehydration before deciding to redirect to login. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWqiUSojMLUoN7mrJqdEbN
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4acbc03b35
commit
b137176c40
@@ -0,0 +1,285 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CostingMethod" AS ENUM ('MANUAL', 'LATEST', 'MOVING_AVERAGE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StockMovementType" AS ENUM ('RECEIPT', 'ISSUE', 'TRANSFER_IN', 'TRANSFER_OUT', 'CONSUMPTION', 'PRODUCTION_IN', 'PRODUCTION_OUT', 'ADJUSTMENT_IN', 'ADJUSTMENT_OUT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StockDocumentStatus" AS ENUM ('DRAFT', 'POSTED', 'VOID');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ControlAccountType" ADD VALUE 'GRN';
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "APInvoiceLine" DROP CONSTRAINT "APInvoiceLine_expenseGLAccountId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "APInvoiceLine" ADD COLUMN "grvLineId" INTEGER,
|
||||
ALTER COLUMN "expenseGLAccountId" DROP NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Warehouse" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"branchSegmentValueId" INTEGER NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Warehouse_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"unitOfMeasure" TEXT NOT NULL,
|
||||
"costingMethod" "CostingMethod" NOT NULL DEFAULT 'MOVING_AVERAGE',
|
||||
"manualCost" DECIMAL(18,4),
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "StockItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockLevel" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"itemId" INTEGER NOT NULL,
|
||||
"warehouseId" INTEGER NOT NULL,
|
||||
"quantityOnHand" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"averageCost" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"lastCost" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "StockLevel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockMovement" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"itemId" INTEGER NOT NULL,
|
||||
"warehouseId" INTEGER NOT NULL,
|
||||
"movementType" "StockMovementType" NOT NULL,
|
||||
"quantity" DECIMAL(18,4) NOT NULL,
|
||||
"unitCost" DECIMAL(18,4) NOT NULL,
|
||||
"totalCost" DECIMAL(18,2) NOT NULL,
|
||||
"movementDate" TIMESTAMP(3) NOT NULL,
|
||||
"description" TEXT,
|
||||
"branchSegmentValueId" INTEGER NOT NULL,
|
||||
"sourceType" TEXT,
|
||||
"sourceId" INTEGER,
|
||||
"journalEntryId" INTEGER,
|
||||
"createdById" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "StockMovement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Recipe" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"outputItemId" INTEGER NOT NULL,
|
||||
"yieldQuantity" DECIMAL(18,4) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Recipe_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RecipeComponent" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"recipeId" INTEGER NOT NULL,
|
||||
"componentItemId" INTEGER NOT NULL,
|
||||
"quantity" DECIMAL(18,4) NOT NULL,
|
||||
|
||||
CONSTRAINT "RecipeComponent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockAdjustment" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"adjustmentNumber" TEXT NOT NULL,
|
||||
"warehouseId" INTEGER NOT NULL,
|
||||
"branchSegmentValueId" INTEGER NOT NULL,
|
||||
"adjustmentDate" TIMESTAMP(3) NOT NULL,
|
||||
"reference" TEXT,
|
||||
"status" "StockDocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"journalEntryId" INTEGER,
|
||||
"createdById" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "StockAdjustment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockAdjustmentLine" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"adjustmentId" INTEGER NOT NULL,
|
||||
"itemId" INTEGER NOT NULL,
|
||||
"systemQuantity" DECIMAL(18,4) NOT NULL,
|
||||
"countedQuantity" DECIMAL(18,4) NOT NULL,
|
||||
"unitCost" DECIMAL(18,4) NOT NULL,
|
||||
"varianceValue" DECIMAL(18,2) NOT NULL,
|
||||
|
||||
CONSTRAINT "StockAdjustmentLine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GoodsReceivedVoucher" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"grvNumber" TEXT NOT NULL,
|
||||
"supplierId" INTEGER NOT NULL,
|
||||
"warehouseId" INTEGER NOT NULL,
|
||||
"branchSegmentValueId" INTEGER NOT NULL,
|
||||
"grvDate" TIMESTAMP(3) NOT NULL,
|
||||
"reference" TEXT,
|
||||
"status" "StockDocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"journalEntryId" INTEGER,
|
||||
"createdById" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GoodsReceivedVoucher_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GoodsReceivedVoucherLine" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"grvId" INTEGER NOT NULL,
|
||||
"itemId" INTEGER NOT NULL,
|
||||
"quantity" DECIMAL(18,4) NOT NULL,
|
||||
"unitPrice" DECIMAL(18,4) NOT NULL,
|
||||
"lineTotal" DECIMAL(18,2) NOT NULL,
|
||||
"matchedQuantity" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "GoodsReceivedVoucherLine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Warehouse_code_key" ON "Warehouse"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Warehouse_branchSegmentValueId_idx" ON "Warehouse"("branchSegmentValueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "StockItem_code_key" ON "StockItem"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "StockLevel_itemId_warehouseId_key" ON "StockLevel"("itemId", "warehouseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockMovement_itemId_warehouseId_idx" ON "StockMovement"("itemId", "warehouseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockMovement_sourceType_sourceId_idx" ON "StockMovement"("sourceType", "sourceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Recipe_code_key" ON "Recipe"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Recipe_outputItemId_key" ON "Recipe"("outputItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RecipeComponent_recipeId_idx" ON "RecipeComponent"("recipeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "StockAdjustment_adjustmentNumber_key" ON "StockAdjustment"("adjustmentNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockAdjustment_warehouseId_idx" ON "StockAdjustment"("warehouseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockAdjustment_branchSegmentValueId_idx" ON "StockAdjustment"("branchSegmentValueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockAdjustment_status_idx" ON "StockAdjustment"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockAdjustmentLine_adjustmentId_idx" ON "StockAdjustmentLine"("adjustmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "GoodsReceivedVoucher_grvNumber_key" ON "GoodsReceivedVoucher"("grvNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoodsReceivedVoucher_supplierId_idx" ON "GoodsReceivedVoucher"("supplierId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoodsReceivedVoucher_warehouseId_idx" ON "GoodsReceivedVoucher"("warehouseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoodsReceivedVoucher_branchSegmentValueId_idx" ON "GoodsReceivedVoucher"("branchSegmentValueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoodsReceivedVoucher_status_idx" ON "GoodsReceivedVoucher"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoodsReceivedVoucherLine_grvId_idx" ON "GoodsReceivedVoucherLine"("grvId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "APInvoiceLine_grvLineId_idx" ON "APInvoiceLine"("grvLineId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "JournalEntryLine" ADD CONSTRAINT "JournalEntryLine_stockMovementId_fkey" FOREIGN KEY ("stockMovementId") REFERENCES "StockMovement"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_expenseGLAccountId_fkey" FOREIGN KEY ("expenseGLAccountId") REFERENCES "GLAccount"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_grvLineId_fkey" FOREIGN KEY ("grvLineId") REFERENCES "GoodsReceivedVoucherLine"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Warehouse" ADD CONSTRAINT "Warehouse_branchSegmentValueId_fkey" FOREIGN KEY ("branchSegmentValueId") REFERENCES "AccountSegmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockLevel" ADD CONSTRAINT "StockLevel_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockLevel" ADD CONSTRAINT "StockLevel_warehouseId_fkey" FOREIGN KEY ("warehouseId") REFERENCES "Warehouse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_warehouseId_fkey" FOREIGN KEY ("warehouseId") REFERENCES "Warehouse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Recipe" ADD CONSTRAINT "Recipe_outputItemId_fkey" FOREIGN KEY ("outputItemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RecipeComponent" ADD CONSTRAINT "RecipeComponent_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RecipeComponent" ADD CONSTRAINT "RecipeComponent_componentItemId_fkey" FOREIGN KEY ("componentItemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockAdjustment" ADD CONSTRAINT "StockAdjustment_warehouseId_fkey" FOREIGN KEY ("warehouseId") REFERENCES "Warehouse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockAdjustment" ADD CONSTRAINT "StockAdjustment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockAdjustmentLine" ADD CONSTRAINT "StockAdjustmentLine_adjustmentId_fkey" FOREIGN KEY ("adjustmentId") REFERENCES "StockAdjustment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockAdjustmentLine" ADD CONSTRAINT "StockAdjustmentLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GoodsReceivedVoucher" ADD CONSTRAINT "GoodsReceivedVoucher_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Supplier"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GoodsReceivedVoucher" ADD CONSTRAINT "GoodsReceivedVoucher_warehouseId_fkey" FOREIGN KEY ("warehouseId") REFERENCES "Warehouse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GoodsReceivedVoucher" ADD CONSTRAINT "GoodsReceivedVoucher_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GoodsReceivedVoucherLine" ADD CONSTRAINT "GoodsReceivedVoucherLine_grvId_fkey" FOREIGN KEY ("grvId") REFERENCES "GoodsReceivedVoucher"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GoodsReceivedVoucherLine" ADD CONSTRAINT "GoodsReceivedVoucherLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "StockItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "ControlAccountType" ADD VALUE 'STOCK_VARIANCE';
|
||||
ALTER TYPE "ControlAccountType" ADD VALUE 'PURCHASE_PRICE_VARIANCE';
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
-- Simplifies Stock down to a catalog (StockItem) + cost centers (Warehouse) + a SOH view
|
||||
-- (StockLevel) for a future Matrix feed to populate. Removes the GRV / Stock Take / Recipe
|
||||
-- production transactional workflows entirely — physical stock transactions belong in Matrix,
|
||||
-- not in this accounting system.
|
||||
|
||||
-- Data cleanup before structural changes (dev/test fixtures from the removed GRV-matching flow).
|
||||
DELETE FROM "APInvoiceLine" WHERE "expenseGLAccountId" IS NULL;
|
||||
|
||||
-- Demote the Stock-related control accounts (1200 Stock, 2050 GRN Accrual, 5200 Stock Variance,
|
||||
-- 5300 Purchase Price Variance) back to plain, manually-postable accounts rather than deleting
|
||||
-- them — several already have real posted JournalEntryLine rows against them from testing, and
|
||||
-- posted ledger history is never deleted. With the GRV/Stock-Take/Recipe workflows gone, nothing
|
||||
-- else in this app owns these accounts, and leaving them locked as control accounts would make
|
||||
-- them permanently unpostable by anyone.
|
||||
UPDATE "GLAccount" SET "isControlAccount" = false, "controlType" = NULL
|
||||
WHERE "controlType" IN ('STOCK', 'GRN', 'STOCK_VARIANCE', 'PURCHASE_PRICE_VARIANCE');
|
||||
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "ControlAccountType_new" AS ENUM ('AR', 'AP', 'TAX_OUTPUT', 'TAX_INPUT', 'BANK', 'RETAINED_EARNINGS');
|
||||
ALTER TABLE "GLAccount" ALTER COLUMN "controlType" TYPE "ControlAccountType_new" USING ("controlType"::text::"ControlAccountType_new");
|
||||
ALTER TYPE "ControlAccountType" RENAME TO "ControlAccountType_old";
|
||||
ALTER TYPE "ControlAccountType_new" RENAME TO "ControlAccountType";
|
||||
DROP TYPE "ControlAccountType_old";
|
||||
COMMIT;
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "APInvoiceLine" DROP CONSTRAINT "APInvoiceLine_expenseGLAccountId_fkey";
|
||||
ALTER TABLE "APInvoiceLine" DROP CONSTRAINT "APInvoiceLine_grvLineId_fkey";
|
||||
ALTER TABLE "GoodsReceivedVoucher" DROP CONSTRAINT "GoodsReceivedVoucher_createdById_fkey";
|
||||
ALTER TABLE "GoodsReceivedVoucher" DROP CONSTRAINT "GoodsReceivedVoucher_supplierId_fkey";
|
||||
ALTER TABLE "GoodsReceivedVoucher" DROP CONSTRAINT "GoodsReceivedVoucher_warehouseId_fkey";
|
||||
ALTER TABLE "GoodsReceivedVoucherLine" DROP CONSTRAINT "GoodsReceivedVoucherLine_grvId_fkey";
|
||||
ALTER TABLE "GoodsReceivedVoucherLine" DROP CONSTRAINT "GoodsReceivedVoucherLine_itemId_fkey";
|
||||
ALTER TABLE "JournalEntryLine" DROP CONSTRAINT "JournalEntryLine_stockMovementId_fkey";
|
||||
ALTER TABLE "Recipe" DROP CONSTRAINT "Recipe_outputItemId_fkey";
|
||||
ALTER TABLE "RecipeComponent" DROP CONSTRAINT "RecipeComponent_componentItemId_fkey";
|
||||
ALTER TABLE "RecipeComponent" DROP CONSTRAINT "RecipeComponent_recipeId_fkey";
|
||||
ALTER TABLE "StockAdjustment" DROP CONSTRAINT "StockAdjustment_createdById_fkey";
|
||||
ALTER TABLE "StockAdjustment" DROP CONSTRAINT "StockAdjustment_warehouseId_fkey";
|
||||
ALTER TABLE "StockAdjustmentLine" DROP CONSTRAINT "StockAdjustmentLine_adjustmentId_fkey";
|
||||
ALTER TABLE "StockAdjustmentLine" DROP CONSTRAINT "StockAdjustmentLine_itemId_fkey";
|
||||
ALTER TABLE "StockMovement" DROP CONSTRAINT "StockMovement_createdById_fkey";
|
||||
ALTER TABLE "StockMovement" DROP CONSTRAINT "StockMovement_itemId_fkey";
|
||||
ALTER TABLE "StockMovement" DROP CONSTRAINT "StockMovement_warehouseId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "APInvoiceLine_grvLineId_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "APInvoiceLine" DROP COLUMN "grvLineId",
|
||||
ALTER COLUMN "expenseGLAccountId" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "JournalEntryLine" DROP COLUMN "stockMovementId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "StockItem" DROP COLUMN "costingMethod",
|
||||
DROP COLUMN "manualCost";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "StockLevel" DROP COLUMN "averageCost",
|
||||
DROP COLUMN "lastCost";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "GoodsReceivedVoucher";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "GoodsReceivedVoucherLine";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Recipe";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "RecipeComponent";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "StockAdjustment";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "StockAdjustmentLine";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "StockMovement";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "CostingMethod";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "StockDocumentStatus";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "StockMovementType";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "APInvoiceLine" ADD CONSTRAINT "APInvoiceLine_expenseGLAccountId_fkey" FOREIGN KEY ("expenseGLAccountId") REFERENCES "GLAccount"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
+48
-4
@@ -105,6 +105,7 @@ model AccountSegmentValue {
|
||||
glAccountLinks GLAccountSegmentValue[]
|
||||
customers Customer[]
|
||||
suppliers Supplier[]
|
||||
warehouses Warehouse[]
|
||||
|
||||
@@unique([segmentDefId, code])
|
||||
}
|
||||
@@ -134,7 +135,6 @@ enum DebitCredit {
|
||||
enum ControlAccountType {
|
||||
AR
|
||||
AP
|
||||
STOCK
|
||||
TAX_OUTPUT
|
||||
TAX_INPUT
|
||||
BANK
|
||||
@@ -253,13 +253,11 @@ model JournalEntryLine {
|
||||
credit Decimal @default(0) @db.Decimal(18, 2)
|
||||
description String?
|
||||
branchSegmentValueId Int?
|
||||
// Sub-ledger cross-references for drill-down. arInvoiceId/apInvoiceId are real FKs now that
|
||||
// AR/AP exist; stockMovementId stays a plain id until Stock lands in its own phase.
|
||||
// Sub-ledger cross-references for drill-down.
|
||||
arInvoiceId Int?
|
||||
arInvoice ARInvoice? @relation(fields: [arInvoiceId], references: [id])
|
||||
apInvoiceId Int?
|
||||
apInvoice APInvoice? @relation(fields: [apInvoiceId], references: [id])
|
||||
stockMovementId Int?
|
||||
currencyCode String @default("BASE")
|
||||
exchangeRateToBase Decimal @default(1) @db.Decimal(18, 6)
|
||||
|
||||
@@ -615,6 +613,52 @@ model APAllocation {
|
||||
@@index([debitNoteId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stock (catalog + cost centers only — see the comment on Warehouse below)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Deliberately minimal: this is accounting software, not a stock/warehouse system. Physical
|
||||
// stock transactions (goods received, stock takes, recipe production) belong in Matrix (the
|
||||
// external hospitality/retail stock system this app is designed to eventually integrate with)
|
||||
// — this app only needs a catalog of items, a set of cost centers to view SOH by, and a place
|
||||
// for whatever quantities Matrix eventually reports. No costing engine, no movement ledger, no
|
||||
// document workflow live here.
|
||||
model Warehouse {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
name String
|
||||
branchSegmentValueId Int
|
||||
branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id])
|
||||
isActive Boolean @default(true)
|
||||
stockLevels StockLevel[]
|
||||
|
||||
@@index([branchSegmentValueId])
|
||||
}
|
||||
|
||||
model StockItem {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
name String
|
||||
unitOfMeasure String
|
||||
isActive Boolean @default(true)
|
||||
stockLevels StockLevel[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// Quantity on hand per item/warehouse — a read target for a future Matrix feed, not written to
|
||||
// from any UI in this app today.
|
||||
model StockLevel {
|
||||
id Int @id @default(autoincrement())
|
||||
itemId Int
|
||||
item StockItem @relation(fields: [itemId], references: [id])
|
||||
warehouseId Int
|
||||
warehouse Warehouse @relation(fields: [warehouseId], references: [id])
|
||||
quantityOnHand Decimal @default(0) @db.Decimal(18, 4)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([itemId, warehouseId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth / RBAC
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+36
-3
@@ -18,6 +18,8 @@ const PERMISSIONS = [
|
||||
'ap.invoice.manage',
|
||||
'ap.debitnote.manage',
|
||||
'ap.payment.manage',
|
||||
'stock.warehouse.manage',
|
||||
'stock.item.manage',
|
||||
] as const;
|
||||
|
||||
const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
|
||||
@@ -35,6 +37,8 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
|
||||
'ap.invoice.manage',
|
||||
'ap.debitnote.manage',
|
||||
'ap.payment.manage',
|
||||
'stock.warehouse.manage',
|
||||
'stock.item.manage',
|
||||
],
|
||||
AR_AP_CLERK: [
|
||||
'ar.customer.manage',
|
||||
@@ -46,6 +50,7 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
|
||||
'ap.debitnote.manage',
|
||||
'ap.payment.manage',
|
||||
],
|
||||
STOCK_CLERK: ['stock.warehouse.manage', 'stock.item.manage'],
|
||||
BRANCH_USER: [],
|
||||
READ_ONLY: [],
|
||||
};
|
||||
@@ -98,14 +103,17 @@ async function main() {
|
||||
{ code: '1100', description: 'Accounts Receivable Control' },
|
||||
{ code: '1200', description: 'Inventory / Stock Control' },
|
||||
{ code: '2000', description: 'Accounts Payable Control' },
|
||||
{ code: '2050', description: 'Goods Received Not Invoiced' },
|
||||
{ code: '2100', description: 'Output Tax Payable' },
|
||||
{ code: '2200', description: 'Input Tax Receivable' },
|
||||
{ code: '2300', description: 'Accrued Expenses' },
|
||||
{ code: '3000', description: 'Retained Earnings' },
|
||||
{ code: '3100', description: 'Share Capital' },
|
||||
{ code: '4000', description: 'Sales Income' },
|
||||
{ code: '5000', description: 'Cost of Goods Sold' },
|
||||
{ code: '5100', description: 'Operating Expenses' },
|
||||
{ code: '5200', description: 'Stock Variance' },
|
||||
{ code: '5300', description: 'Purchase Price Variance' },
|
||||
];
|
||||
const masterByCode: Record<string, { id: number }> = {};
|
||||
for (const value of masterValues) {
|
||||
@@ -157,14 +165,19 @@ async function main() {
|
||||
}> = [
|
||||
{ masterCode: '1000', accountName: 'Bank / Cash', accountType: 'ASSET', normalBalance: 'DEBIT', controlType: 'BANK' },
|
||||
{ masterCode: '1100', accountName: 'Accounts Receivable Control', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'AR' },
|
||||
{ masterCode: '1200', accountName: 'Inventory / Stock Control', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'STOCK' },
|
||||
// Not a control account: no subledger in this app owns stock value — see the Stock section
|
||||
// of prisma/schema.prisma. Postable directly via manual journal like any other GL account.
|
||||
{ masterCode: '1200', accountName: 'Inventory / Stock Control', accountType: 'ASSET', normalBalance: 'DEBIT' },
|
||||
{ masterCode: '2000', accountName: 'Accounts Payable Control', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'AP' },
|
||||
{ masterCode: '2050', accountName: 'Goods Received Not Invoiced', accountType: 'LIABILITY', normalBalance: 'CREDIT' },
|
||||
{ masterCode: '2100', accountName: 'Output Tax Payable', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'TAX_OUTPUT' },
|
||||
{ masterCode: '2200', accountName: 'Input Tax Receivable', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'TAX_INPUT' },
|
||||
{ masterCode: '2300', accountName: 'Accrued Expenses', accountType: 'LIABILITY', normalBalance: 'CREDIT' },
|
||||
{ masterCode: '4000', accountName: 'Sales Income', accountType: 'INCOME', normalBalance: 'CREDIT' },
|
||||
{ masterCode: '5000', accountName: 'Cost of Goods Sold', accountType: 'EXPENSE', normalBalance: 'DEBIT' },
|
||||
{ masterCode: '5100', accountName: 'Operating Expenses', accountType: 'EXPENSE', normalBalance: 'DEBIT' },
|
||||
{ masterCode: '5200', accountName: 'Stock Variance', accountType: 'EXPENSE', normalBalance: 'DEBIT' },
|
||||
{ masterCode: '5300', accountName: 'Purchase Price Variance', accountType: 'EXPENSE', normalBalance: 'DEBIT' },
|
||||
];
|
||||
|
||||
for (const branchCode of Object.keys(branchByCode)) {
|
||||
@@ -189,7 +202,13 @@ async function main() {
|
||||
],
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
update: {
|
||||
accountName: `${account.accountName} — ${branchCode}`,
|
||||
accountType: account.accountType,
|
||||
normalBalance: account.normalBalance,
|
||||
isControlAccount: account.isControlAccount ?? false,
|
||||
controlType: account.controlType ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -218,7 +237,7 @@ async function main() {
|
||||
],
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
update: { accountName: account.accountName, controlType: account.controlType ?? null },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,6 +277,20 @@ async function main() {
|
||||
create: { code: 'CASH_PAYMENT', name: 'Cash Payments Journal', journalType: JournalType.CASH_PAYMENT },
|
||||
update: {},
|
||||
});
|
||||
await prisma.journal.upsert({
|
||||
where: { code: 'STOCK' },
|
||||
create: { code: 'STOCK', name: 'Stock Journal', journalType: JournalType.STOCK },
|
||||
update: {},
|
||||
});
|
||||
|
||||
console.log('Seeding warehouses...');
|
||||
for (const branchCode of Object.keys(branchByCode)) {
|
||||
await prisma.warehouse.upsert({
|
||||
where: { code: `WH-${branchCode}` },
|
||||
create: { code: `WH-${branchCode}`, name: `Main Warehouse — ${branchCode}`, branchSegmentValueId: branchByCode[branchCode].id },
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Seeding tax codes...');
|
||||
const taxCodes = [
|
||||
|
||||
Reference in New Issue
Block a user