Преглед изворни кода

feat: enhance warehouse with unit mass/count and deduct feature

unknown пре 21 часа
родитељ
комит
e74339644f

+ 21 - 0
backend/alter_db_warehouse_units.py

@@ -0,0 +1,21 @@
+import db
+
+def migrate():
+    print("Migrating warehouse_stock table to support units and mass...")
+    # Add unit_mass and units_count columns
+    queries = [
+        "ALTER TABLE warehouse_stock ADD COLUMN unit_mass decimal(10,3) DEFAULT 1.000",
+        "ALTER TABLE warehouse_stock ADD COLUMN units_count int DEFAULT 0",
+        # Optionally update quantity to be units_count * unit_mass? 
+        # For now, let's just add them.
+    ]
+    
+    for q in queries:
+        try:
+            db.execute_commit(q)
+            print(f"Executed: {q}")
+        except Exception as e:
+            print(f"Error executing {q}: {e}")
+
+if __name__ == "__main__":
+    migrate()

+ 11 - 3
backend/routers/warehouse.py

@@ -52,10 +52,10 @@ async def add_stock_item(
     admin: dict = Depends(require_admin)
 ):
     query = """
-        INSERT INTO warehouse_stock (material_id, color_name, quantity, notes, is_active)
-        VALUES (%s, %s, %s, %s, %s)
+        INSERT INTO warehouse_stock (material_id, color_name, quantity, unit_mass, units_count, notes, is_active)
+        VALUES (%s, %s, %s, %s, %s, %s, %s)
     """
-    params = (data.material_id, data.color_name, data.quantity, data.notes, data.is_active)
+    params = (data.material_id, data.color_name, data.quantity, data.unit_mass, data.units_count, data.notes, data.is_active)
     
     item_id = db.execute_commit(query, params)
     
@@ -76,6 +76,14 @@ async def update_stock_item(
     if data.quantity is not None:
         update_fields.append("quantity = %s")
         params.append(data.quantity)
+
+    if data.unit_mass is not None:
+        update_fields.append("unit_mass = %s")
+        params.append(data.unit_mass)
+
+    if data.units_count is not None:
+        update_fields.append("units_count = %s")
+        params.append(data.units_count)
     
     if data.notes is not None:
         update_fields.append("notes = %s")

+ 5 - 1
backend/schemas.py

@@ -262,7 +262,9 @@ class TokenVerify(BaseModel):
 class WarehouseItemBase(BaseModel):
     material_id: int
     color_name: str
-    quantity: float = 0.0
+    quantity: float = 0.0 # Total weight
+    unit_mass: float = 1.0 # Weight of one unit (e.g. 1kg spool)
+    units_count: int = 0   # Number of units (e.g. 10 spools)
     notes: Optional[str] = None
     is_active: bool = True
 
@@ -271,6 +273,8 @@ class WarehouseItemCreate(WarehouseItemBase):
 
 class WarehouseItemUpdate(BaseModel):
     quantity: Optional[float] = None
+    unit_mass: Optional[float] = None
+    units_count: Optional[int] = None
     notes: Optional[str] = None
     is_active: Optional[bool] = None
 

+ 60 - 7
src/components/admin/WarehouseSection.vue

@@ -25,7 +25,9 @@
             <tr class="bg-muted/30 border-b border-border/50 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
               <th class="p-4">{{ t("admin.fields.material") }}</th>
               <th class="p-4">{{ t("admin.fields.colors") }}</th>
-              <th class="p-4 text-center">{{ t("admin.fields.quantity") }}</th>
+              <th class="p-4 text-center">{{ t("admin.fields.unitMass") }}</th>
+              <th class="p-4 text-center">{{ t("admin.fields.unitsCount") }}</th>
+              <th class="p-4 text-center">{{ t("admin.fields.quantity") }} (kg)</th>
               <th class="p-4 text-center">{{ t("admin.fields.status") }}</th>
               <th class="p-4 text-right">{{ t("admin.labels.actions") }}</th>
             </tr>
@@ -45,7 +47,18 @@
                 </div>
               </td>
               <td class="p-4 text-center">
-                <span class="text-sm font-mono font-bold" :class="item.quantity <= 0 ? 'text-rose-500' : 'text-emerald-500'">
+                <span class="text-xs font-mono">{{ item.unit_mass }}</span>
+              </td>
+              <td class="p-4 text-center">
+                <div class="flex items-center justify-center gap-2">
+                  <span class="text-xs font-bold">{{ item.units_count }}</span>
+                  <button v-if="item.units_count > 0" @click="handleDeduct(item)" class="p-1 hover:bg-rose-500/10 rounded-md text-rose-500 transition-colors" title="Deduct 1 unit">
+                    <Minus class="w-3 h-3" />
+                  </button>
+                </div>
+              </td>
+              <td class="p-4 text-center">
+                <span class="text-sm font-mono font-bold" :class="item.quantity <= 0.1 ? 'text-rose-500' : 'text-emerald-500'">
                   {{ item.quantity }}
                 </span>
               </td>
@@ -102,8 +115,19 @@
 
           <div class="grid grid-cols-2 gap-4">
             <div class="space-y-1">
-              <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.quantity') }}</label>
-              <input v-model.number="form.quantity" type="number" step="0.1" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
+              <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.unitMass') }}</label>
+              <input v-model.number="form.unit_mass" type="number" step="0.001" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
+            </div>
+            <div class="space-y-1">
+              <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.unitsCount') }}</label>
+              <input v-model.number="form.units_count" type="number" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
+            </div>
+          </div>
+
+          <div class="grid grid-cols-2 gap-4">
+            <div class="space-y-1">
+              <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.quantity') }} (kg)</label>
+              <input v-model.number="form.quantity" type="number" step="0.001" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
             </div>
             <div class="space-y-1 flex flex-col justify-end pb-2">
               <label class="flex items-center gap-2 cursor-pointer group">
@@ -142,7 +166,7 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted } from "vue";
 import { useI18n } from "vue-i18n";
-import { Package, Plus, PackageOpen, Check, Edit2, Trash2 } from "lucide-vue-next";
+import { Package, Plus, PackageOpen, Check, Edit2, Trash2, Minus } from "lucide-vue-next";
 import { toast } from "vue-sonner";
 import { 
   adminGetWarehouseStock, 
@@ -166,11 +190,19 @@ const editingId = ref<number | null>(null);
 const form = reactive({
   material_id: 0,
   color_name: "",
-  quantity: 1,
+  quantity: 1.0,
+  unit_mass: 1.0,
+  units_count: 1,
   is_active: true,
   notes: ""
 });
 
+watch(() => [form.unit_mass, form.units_count], ([m, c]) => {
+  if (!editingId.value) {
+     form.quantity = Number((m * c).toFixed(3));
+  }
+});
+
 onMounted(() => {
   fetchStock();
   if (props.materials.length > 0) {
@@ -188,6 +220,25 @@ async function fetchStock() {
   }
 }
 
+async function handleDeduct(item: any) {
+  if (item.units_count <= 0) return;
+  
+  const newCount = item.units_count - 1;
+  const newTotal = Number((Math.max(0, item.quantity - item.unit_mass)).toFixed(3));
+  
+  try {
+    await adminUpdateWarehouseStock(item.id, { 
+      units_count: newCount,
+      quantity: newTotal
+    });
+    item.units_count = newCount;
+    item.quantity = newTotal;
+    toast.success(t('admin.toasts.materialSaved'));
+  } catch (err: any) {
+    toast.error(err.message);
+  }
+}
+
 async function toggleStatus(item: any) {
   try {
     await adminUpdateWarehouseStock(item.id, { is_active: !item.is_active });
@@ -204,6 +255,8 @@ function handleEdit(item: any) {
     material_id: item.material_id,
     color_name: item.color_name,
     quantity: item.quantity,
+    unit_mass: item.unit_mass,
+    units_count: item.units_count,
     is_active: item.is_active,
     notes: item.notes || ""
   });
@@ -211,7 +264,7 @@ function handleEdit(item: any) {
 }
 
 async function handleDelete(id: number) {
-  if (!confirm(t('admin.questions.deletePhoto'))) return; // Repurposing confirm
+  if (!confirm(t('admin.questions.deletePhoto'))) return;
   try {
     await adminDeleteWarehouseStock(id);
     toast.success(t('admin.toasts.materialDeleted'));

+ 12 - 0
src/locales/master_admin/fields.json

@@ -353,6 +353,18 @@
       "me": "Korisnik",
       "ru": "Пользователь",
       "ua": "Користувач"
+    },
+    "unitMass": {
+      "en": "Unit Mass (kg)",
+      "me": "Masa jedinice (kg)",
+      "ru": "Вес единицы (кг)",
+      "ua": "Вага одиниці (кг)"
+    },
+    "unitsCount": {
+      "en": "Units Count",
+      "me": "Broj komada",
+      "ru": "Кол-во штук",
+      "ua": "Кіл-ть штук"
     }
   }
 }

+ 12 - 0
src/locales/translations.admin.json

@@ -506,6 +506,18 @@
         "me": "Korisnik",
         "ru": "Пользователь",
         "ua": "Користувач"
+      },
+      "unitMass": {
+        "en": "Unit Mass (kg)",
+        "me": "Masa jedinice (kg)",
+        "ru": "Вес единицы (кг)",
+        "ua": "Вага одиниці (кг)"
+      },
+      "unitsCount": {
+        "en": "Units Count",
+        "me": "Broj komada",
+        "ru": "Кол-во штук",
+        "ua": "Кіл-ть штук"
       }
     },
     "filters": {