feat(stock): stok kategorileri, ürün tipleri, hareketler ve FIFO SKT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,11 @@ export const AUDIT_ACTIONS = [
|
||||
"inkind.delete",
|
||||
"asset.delete",
|
||||
"asset.movement.delete",
|
||||
"stock.category.archive",
|
||||
"stock.category.unarchive",
|
||||
"stock.product.archive",
|
||||
"stock.product.unarchive",
|
||||
"stock.movement.delete",
|
||||
] as const;
|
||||
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { model, models, Schema, type InferSchemaType, type Model } from "mongoose";
|
||||
|
||||
/**
|
||||
* Ürün tipi (genel ürün, markadan bağımsız ihtiyaç: "Toz Deterjan"). Stok bu seviyede tutulur.
|
||||
* Miktarlar binde bir (QUANTITY_SCALE). Birim hazır listeden ya da özel ("yıkama").
|
||||
*/
|
||||
const productTypeSchema = new Schema(
|
||||
{
|
||||
householdId: { type: Schema.Types.ObjectId, required: true },
|
||||
categoryId: { type: Schema.Types.ObjectId, required: true },
|
||||
name: { type: String, required: true, trim: true, maxlength: 80 },
|
||||
nameKey: { type: String, required: true },
|
||||
unit: { type: String, required: true, trim: true, maxlength: 20 },
|
||||
/** Hedef (tam) stok miktarı. */
|
||||
targetQuantity: { type: Number, required: true, min: 0 },
|
||||
/** Kritik eşik, hedefin yüzdesi (varsayılan 20). */
|
||||
thresholdPct: { type: Number, default: 20, min: 0, max: 100 },
|
||||
/** Saklama yeri (kiler, banyo dolabı …). */
|
||||
location: { type: String, default: "", maxlength: 80 },
|
||||
/** "1 azalt" düğmesinin düşeceği miktar. */
|
||||
step: { type: Number, default: 1000, min: 1 },
|
||||
/** Sayım sıklığı (gün); null ise kategoriden devralınır (Faz 11b). */
|
||||
countEveryDays: { type: Number, default: null, min: 1, max: 365 },
|
||||
note: { type: String, default: "", maxlength: 300 },
|
||||
archivedAt: { type: Date, default: null },
|
||||
seedRef: { type: String, default: null },
|
||||
createdBy: { type: Schema.Types.ObjectId, required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, required: true },
|
||||
},
|
||||
{ timestamps: true, collection: "productTypes" },
|
||||
);
|
||||
|
||||
productTypeSchema.index({ householdId: 1, archivedAt: 1, categoryId: 1 });
|
||||
productTypeSchema.index(
|
||||
{ householdId: 1, seedRef: 1 },
|
||||
{ unique: true, partialFilterExpression: { seedRef: { $type: "string" } } },
|
||||
);
|
||||
|
||||
export type ProductType = InferSchemaType<typeof productTypeSchema>;
|
||||
|
||||
export const ProductTypeModel: Model<ProductType> =
|
||||
(models.ProductType as Model<ProductType> | undefined) ??
|
||||
model<ProductType>("ProductType", productTypeSchema);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { model, models, Schema, type InferSchemaType, type Model } from "mongoose";
|
||||
|
||||
/**
|
||||
* Stok kategori ağacı (Bölüm 6.10), gelir/gider kategorilerinden ayrı. Silme yok; arşivleme.
|
||||
* `expenseCategoryId`: alışveriş tamamlanınca (Faz 13) kalemlerin yazılacağı gider kategorisi;
|
||||
* null ise üst kategoriden devralınır. `countEveryDays`: periyodik sayım sıklığı (Faz 11b);
|
||||
* null ise üstten devralınır.
|
||||
*/
|
||||
const stockCategorySchema = new Schema(
|
||||
{
|
||||
householdId: { type: Schema.Types.ObjectId, required: true },
|
||||
name: { type: String, required: true, trim: true, maxlength: 80 },
|
||||
nameKey: { type: String, required: true },
|
||||
parentId: { type: Schema.Types.ObjectId, default: null },
|
||||
ancestors: { type: [Schema.Types.ObjectId], default: [] },
|
||||
order: { type: Number, default: 0 },
|
||||
expenseCategoryId: { type: Schema.Types.ObjectId, default: null },
|
||||
countEveryDays: { type: Number, default: null, min: 1, max: 365 },
|
||||
archivedAt: { type: Date, default: null },
|
||||
defaultKey: { type: String, default: null },
|
||||
createdBy: { type: Schema.Types.ObjectId, required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, required: true },
|
||||
},
|
||||
{ timestamps: true, collection: "stockCategories" },
|
||||
);
|
||||
|
||||
stockCategorySchema.index({ householdId: 1, parentId: 1, order: 1 });
|
||||
stockCategorySchema.index(
|
||||
{ householdId: 1, defaultKey: 1 },
|
||||
{ unique: true, partialFilterExpression: { defaultKey: { $type: "string" } } },
|
||||
);
|
||||
|
||||
export type StockCategory = InferSchemaType<typeof stockCategorySchema>;
|
||||
|
||||
export const StockCategoryModel: Model<StockCategory> =
|
||||
(models.StockCategory as Model<StockCategory> | undefined) ??
|
||||
model<StockCategory>("StockCategory", stockCategorySchema);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { model, models, Schema, type InferSchemaType, type Model } from "mongoose";
|
||||
import { STOCK_MOVEMENT_KINDS, WASTE_REASONS } from "@/shared/stock";
|
||||
|
||||
/**
|
||||
* Stok hareketi: alım, tüketim, fire (neden ile), sayım düzeltmesi. Mevcut miktar hareketlerden
|
||||
* türetilir. Miktar binde bir; yalnızca sayım düzeltmesi işaretlidir. Sıra: tarih, oluşturulma.
|
||||
*/
|
||||
const stockMovementSchema = new Schema(
|
||||
{
|
||||
householdId: { type: Schema.Types.ObjectId, required: true },
|
||||
productTypeId: { type: Schema.Types.ObjectId, required: true },
|
||||
kind: { type: String, enum: STOCK_MOVEMENT_KINDS, required: true },
|
||||
/** "YYYY-MM-DD" (İstanbul). */
|
||||
date: { type: String, required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
wasteReason: { type: String, enum: [...WASTE_REASONS, null], default: null },
|
||||
/** Son kullanma tarihi (yalnızca alımda, isteğe bağlı). */
|
||||
expiryDate: { type: String, default: null },
|
||||
note: { type: String, default: "", maxlength: 300 },
|
||||
/** İlk kurulumda girilen başlangıç miktarı (sayım düzeltmesi olarak). */
|
||||
initial: { type: Boolean, default: false },
|
||||
/** Sayım oturumu (Faz 11b). */
|
||||
countId: { type: Schema.Types.ObjectId, default: null },
|
||||
deletedAt: { type: Date, default: null },
|
||||
seedRef: { type: String, default: null },
|
||||
createdBy: { type: Schema.Types.ObjectId, required: true },
|
||||
},
|
||||
{ timestamps: true, collection: "stockMovements" },
|
||||
);
|
||||
|
||||
stockMovementSchema.index({
|
||||
householdId: 1,
|
||||
productTypeId: 1,
|
||||
deletedAt: 1,
|
||||
date: 1,
|
||||
createdAt: 1,
|
||||
});
|
||||
stockMovementSchema.index({ householdId: 1, kind: 1, date: -1 });
|
||||
stockMovementSchema.index(
|
||||
{ householdId: 1, seedRef: 1 },
|
||||
{ unique: true, partialFilterExpression: { seedRef: { $type: "string" } } },
|
||||
);
|
||||
|
||||
export type StockMovement = InferSchemaType<typeof stockMovementSchema>;
|
||||
|
||||
export const StockMovementModel: Model<StockMovement> =
|
||||
(models.StockMovement as Model<StockMovement> | undefined) ??
|
||||
model<StockMovement>("StockMovement", stockMovementSchema);
|
||||
Reference in New Issue
Block a user