Pārlūkot izejas kodu

feat: implemented warehouse stock PDF report generation

unknown 21 stundas atpakaļ
vecāks
revīzija
72657a839b

+ 8 - 0
backend/routers/warehouse.py

@@ -5,9 +5,17 @@ import schemas
 from dependencies import require_admin
 import services.event_hooks as event_hooks
 from services.audit_service import audit_service
+from services.warehouse_report_service import warehouse_report_service
 
 router = APIRouter(prefix="/admin/warehouse", tags=["warehouse"])
 
+@router.get("/report")
+async def generate_stock_report(
+    admin: dict = Depends(require_admin)
+):
+    url = warehouse_report_service.generate_report()
+    return {"url": url}
+
 @router.get("/stock", response_model=dict)
 async def get_warehouse_stock(
     page: int = Query(1, ge=1),

+ 66 - 0
backend/services/warehouse_report_service.py

@@ -0,0 +1,66 @@
+import os
+import db
+import config
+from fpdf import FPDF
+from datetime import datetime
+
+class WarehouseReportService:
+    @staticmethod
+    def generate_report():
+        # Fetch all stock items with material names
+        query = """
+            SELECT w.*, m.name_en as material_name, m.name_ru as material_name_ru
+            FROM warehouse_stock w
+            JOIN materials m ON w.material_id = m.id
+            ORDER BY m.name_en, w.color_name
+        """
+        stock = db.execute_query(query)
+        
+        pdf = FPDF(orientation='P', unit='mm', format='A4')
+        pdf.add_page()
+        
+        # Header
+        pdf.set_font("helvetica", "B", 16)
+        pdf.cell(0, 10, "Warehouse Stock Report", ln=True, align='C')
+        pdf.set_font("helvetica", "", 10)
+        pdf.cell(0, 5, f"Date: {datetime.now().strftime('%d.%m.%Y %H:%M')}", ln=True, align='C')
+        pdf.ln(10)
+        
+        # Table Header
+        pdf.set_fill_color(240, 240, 240)
+        pdf.set_font("helvetica", "B", 10)
+        pdf.cell(60, 10, "Material", border=1, fill=True)
+        pdf.cell(40, 10, "Color", border=1, fill=True)
+        pdf.cell(30, 10, "Unit Mass", border=1, fill=True, align='C')
+        pdf.cell(30, 10, "Units", border=1, fill=True, align='C')
+        pdf.cell(30, 10, "Total (kg)", border=1, fill=True, align='C')
+        pdf.ln()
+        
+        # Table Body
+        pdf.set_font("helvetica", "", 10)
+        total_weight = 0
+        for item in stock:
+            pdf.cell(60, 10, str(item['material_name']), border=1)
+            pdf.cell(40, 10, str(item['color_name']), border=1)
+            pdf.cell(30, 10, f"{item['unit_mass']:.3f}", border=1, align='C')
+            pdf.cell(30, 10, str(item['units_count']), border=1, align='C')
+            pdf.cell(30, 10, f"{item['quantity']:.3f}", border=1, align='C')
+            pdf.ln()
+            total_weight += float(item['quantity'])
+            
+        # Total
+        pdf.set_font("helvetica", "B", 10)
+        pdf.cell(160, 10, "GRAND TOTAL WEIGHT (kg):", border=1, align='R')
+        pdf.cell(30, 10, f"{total_weight:.3f}", border=1, align='C')
+        
+        # Output
+        filename = f"warehouse_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
+        report_dir = os.path.join(config.UPLOAD_DIR, "reports")
+        os.makedirs(report_dir, exist_ok=True)
+        filepath = os.path.join(report_dir, filename)
+        
+        pdf.output(filepath)
+        
+        return os.path.join("uploads", "reports", filename).replace("\\", "/")
+
+warehouse_report_service = WarehouseReportService()

+ 25 - 6
src/components/admin/WarehouseSection.vue

@@ -6,10 +6,16 @@
         <Package class="w-5 h-5 text-primary" />
         <h2 class="text-sm font-black uppercase tracking-widest">{{ t("admin.tabs.warehouse") }}</h2>
       </div>
-      <button @click="showAddModal = true" class="flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl text-xs font-bold hover:shadow-glow transition-all">
-        <Plus class="w-4 h-4" />
-        {{ t("admin.actions.add") }}
-      </button>
+      <div class="flex items-center gap-2">
+        <button @click="handleDownloadReport" class="flex items-center gap-2 bg-card border border-border/50 text-muted-foreground px-4 py-2 rounded-xl text-xs font-bold hover:border-primary/50 transition-all">
+          <FileText class="w-4 h-4 text-primary" />
+          {{ t("admin.actions.report") || 'Report' }}
+        </button>
+        <button @click="showAddModal = true" class="flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl text-xs font-bold hover:shadow-glow transition-all">
+          <Plus class="w-4 h-4" />
+          {{ t("admin.actions.add") }}
+        </button>
+      </div>
     </div>
 
     <!-- Stock Table -->
@@ -166,13 +172,15 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, watch } from "vue";
 import { useI18n } from "vue-i18n";
-import { Package, Plus, PackageOpen, Check, Edit2, Trash2, Minus } from "lucide-vue-next";
+import { Package, Plus, PackageOpen, Check, Edit2, Trash2, Minus, FileText } from "lucide-vue-next";
 import { toast } from "vue-sonner";
 import { 
   adminGetWarehouseStock, 
   adminAddWarehouseStock, 
   adminUpdateWarehouseStock, 
-  adminDeleteWarehouseStock 
+  adminDeleteWarehouseStock,
+  adminGetWarehouseReport,
+  RESOURCES_BASE_URL 
 } from "@/lib/api";
 
 const { t } = useI18n();
@@ -211,6 +219,17 @@ onMounted(() => {
   }
 });
 
+async function handleDownloadReport() {
+  try {
+    const res = await adminGetWarehouseReport();
+    if (res.url) {
+      window.open(`${RESOURCES_BASE_URL}/${res.url}`, '_blank');
+    }
+  } catch (err: any) {
+    toast.error(err.message);
+  }
+}
+
 async function fetchStock() {
   try {
     const res = await adminGetWarehouseStock(currentPage.value);

+ 9 - 0
src/lib/api.ts

@@ -737,3 +737,12 @@ export const adminDeleteWarehouseStock = async (itemId: number) => {
   if (!response.ok) throw new Error("Failed to delete stock item");
   return response.json();
 };
+
+export const adminGetWarehouseReport = async () => {
+  const token = localStorage.getItem("token");
+  const response = await fetch(`${API_BASE_URL}/admin/warehouse/report`, {
+    headers: { 'Authorization': `Bearer ${token}` }
+  });
+  if (!response.ok) throw new Error("Failed to generate report");
+  return response.json();
+};

+ 6 - 0
src/locales/master_admin/actions.json

@@ -131,6 +131,12 @@
       "me": "Vidi originalne parametre",
       "ru": "Оригинал",
       "ua": "Оригінал"
+    },
+    "report": {
+      "en": "Download Report",
+      "me": "Preuzmi izvještaj",
+      "ru": "Скачать отчет",
+      "ua": "Завантажити звіт"
     }
   }
 }

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

@@ -132,6 +132,12 @@
         "me": "Vidi originalne parametre",
         "ru": "Оригинал",
         "ua": "Оригінал"
+      },
+      "report": {
+        "en": "Download Report",
+        "me": "Preuzmi izvještaj",
+        "ru": "Скачать отчет",
+        "ua": "Завантажити звіт"
       }
     },
     "addNew": {