Jelajahi Sumber

Add currency symbol, hide projected cost for no costs configured, add cost rounding to decimal point, fixed isExternal logic

Matteo Parenti 6 bulan lalu
induk
melakukan
9f19e8a2ff

+ 12 - 3
backend/app/api/routes/archives.py

@@ -2,6 +2,7 @@ import io
 import json
 import json
 import logging
 import logging
 import zipfile
 import zipfile
+from decimal import ROUND_HALF_UP, Decimal
 from pathlib import Path
 from pathlib import Path
 
 
 from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
 from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
@@ -865,18 +866,26 @@ async def rescan_archive(
         )
         )
         usage_cost = usage_result.scalar()
         usage_cost = usage_result.scalar()
         if usage_cost is not None and usage_cost > 0:
         if usage_cost is not None and usage_cost > 0:
-            archive.cost = round(usage_cost, 2)
+            archive.cost = float(Decimal(str(usage_cost)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
         else:
         else:
             primary_type = archive.filament_type.split(",")[0].strip()
             primary_type = archive.filament_type.split(",")[0].strip()
             filament_result = await db.execute(select(Filament).where(Filament.type == primary_type).limit(1))
             filament_result = await db.execute(select(Filament).where(Filament.type == primary_type).limit(1))
             filament = filament_result.scalar_one_or_none()
             filament = filament_result.scalar_one_or_none()
             if filament:
             if filament:
-                archive.cost = round((archive.filament_used_grams / 1000) * filament.cost_per_kg, 2)
+                archive.cost = float(
+                    Decimal(str((archive.filament_used_grams / 1000) * filament.cost_per_kg)).quantize(
+                        Decimal("0.01"), rounding=ROUND_HALF_UP
+                    )
+                )
             else:
             else:
                 # Use default filament cost from settings
                 # Use default filament cost from settings
                 default_cost_setting = await get_setting(db, "default_filament_cost")
                 default_cost_setting = await get_setting(db, "default_filament_cost")
                 default_cost_per_kg = float(default_cost_setting) if default_cost_setting else 25.0
                 default_cost_per_kg = float(default_cost_setting) if default_cost_setting else 25.0
-                archive.cost = round((archive.filament_used_grams / 1000) * default_cost_per_kg, 2)
+                archive.cost = float(
+                    Decimal(str((archive.filament_used_grams / 1000) * default_cost_per_kg)).quantize(
+                        Decimal("0.01"), rounding=ROUND_HALF_UP
+                    )
+                )
 
 
     await db.commit()
     await db.commit()
     await db.refresh(archive)
     await db.refresh(archive)

+ 18 - 6
frontend/src/components/PrintModal/FilamentMapping.tsx

@@ -46,9 +46,7 @@ export function FilamentMapping({
     const map = new Map<number, number | null>();
     const map = new Map<number, number | null>();
     for (const assignment of assignments || []) {
     for (const assignment of assignments || []) {
       const isExternal = assignment.ams_id === 255;
       const isExternal = assignment.ams_id === 255;
-      const globalTrayId = isExternal
-        ? 254 + assignment.tray_id
-        : getGlobalTrayId(assignment.ams_id, assignment.tray_id, false);
+      const globalTrayId = getGlobalTrayId(assignment.ams_id, assignment.tray_id, isExternal);
       map.set(globalTrayId, assignment.spool?.cost_per_kg ?? null);
       map.set(globalTrayId, assignment.spool?.cost_per_kg ?? null);
     }
     }
     return map;
     return map;
@@ -216,9 +214,23 @@ export function FilamentMapping({
               )}
               )}
             </div>
             </div>
           ))}
           ))}
-          <div className="text-xs text-bambu-gray">
-            {t('printModal.totalCost')} <span className="text-white">{currencySymbol}{totalCost.toFixed(2)}</span>
-          </div>
+          {(() => {
+            // Check if any tray has a configured cost_per_kg
+            const hasAnyCost = Array.from(trayCostMap.values()).some((v) => v != null && v > 0);
+            if (totalCost > 0 || hasAnyCost) {
+              return (
+                <div className="text-xs text-bambu-gray">
+                  {t('printModal.totalCost')} <span className="text-white">{currencySymbol}{totalCost.toFixed(2)}</span>
+                </div>
+              );
+            } else {
+              return (
+                <div className="text-xs text-bambu-gray">
+                  {t('printModal.totalCost')} <span className="text-white">N/A</span>
+                </div>
+              );
+            }
+          })()}
           {hasTypeMismatch && (
           {hasTypeMismatch && (
             <p className="text-xs text-orange-400 mt-2">Required filament type not found in printer.</p>
             <p className="text-xs text-orange-400 mt-2">Required filament type not found in printer.</p>
           )}
           )}

+ 3 - 1
frontend/src/components/SpoolFormModal.tsx

@@ -22,9 +22,10 @@ interface SpoolFormModalProps {
   onClose: () => void;
   onClose: () => void;
   spool?: InventorySpool | null;
   spool?: InventorySpool | null;
   printersWithCalibrations?: PrinterWithCalibrations[];
   printersWithCalibrations?: PrinterWithCalibrations[];
+  currencySymbol: string;
 }
 }
 
 
-export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibrations = [] }: SpoolFormModalProps) {
+export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibrations = [], currencySymbol }: SpoolFormModalProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { showToast } = useToast();
@@ -471,6 +472,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
                   formData={formData}
                   formData={formData}
                   updateField={updateField}
                   updateField={updateField}
                   spoolCatalog={spoolCatalog}
                   spoolCatalog={spoolCatalog}
+                  currencySymbol={currencySymbol}
                 />
                 />
               </div>
               </div>
 
 

+ 4 - 1
frontend/src/components/spool-form/AdditionalSection.tsx

@@ -172,6 +172,7 @@ export function AdditionalSection({
   formData,
   formData,
   updateField,
   updateField,
   spoolCatalog,
   spoolCatalog,
+  currencySymbol,
 }: AdditionalSectionProps) {
 }: AdditionalSectionProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { showToast } = useToast();
   const { showToast } = useToast();
@@ -264,6 +265,7 @@ export function AdditionalSection({
         <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.costPerKg', 'Cost per kg')}</label>
         <label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.costPerKg', 'Cost per kg')}</label>
         <div className="flex items-center gap-2">
         <div className="flex items-center gap-2">
           <div className="relative flex-1">
           <div className="relative flex-1">
+            <span className="absolute left-3 top-1/2 -translate-y-1/2 text-bambu-gray text-sm pointer-events-none">{currencySymbol}</span>
             <input
             <input
               type="number"
               type="number"
               value={formData.cost_per_kg ?? ''}
               value={formData.cost_per_kg ?? ''}
@@ -274,7 +276,8 @@ export function AdditionalSection({
                 const value = e.target.value === '' ? null : parseFloat(e.target.value);
                 const value = e.target.value === '' ? null : parseFloat(e.target.value);
                 updateField('cost_per_kg', value);
                 updateField('cost_per_kg', value);
               }}
               }}
-              className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
+              style={{ paddingLeft: `${Math.max(2, currencySymbol.length * 0.6 + 1)}rem` }}
+              className="w-full py-2 pr-3 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
             />
             />
           </div>
           </div>
         </div>
         </div>

+ 1 - 0
frontend/src/components/spool-form/types.ts

@@ -91,6 +91,7 @@ export interface ColorSectionProps extends SectionProps {
 // Additional section props
 // Additional section props
 export interface AdditionalSectionProps extends SectionProps {
 export interface AdditionalSectionProps extends SectionProps {
   spoolCatalog: { id: number; name: string; weight: number }[];
   spoolCatalog: { id: number; name: string; weight: number }[];
+  currencySymbol: string;
 }
 }
 
 
 // PA Profile section props
 // PA Profile section props

+ 1 - 1
frontend/src/pages/ArchivesPage.tsx

@@ -923,7 +923,7 @@ function ArchiveCard({
               {archive.filament_used_grams.toFixed(1)}g
               {archive.filament_used_grams.toFixed(1)}g
             </div>
             </div>
           )}
           )}
-          {archive.cost != null && archive.cost > 0 && (
+          {archive.cost != null && (
             <div className="flex items-center gap-1.5 text-bambu-gray">
             <div className="flex items-center gap-1.5 text-bambu-gray">
               <Coins className="w-3 h-3" />
               <Coins className="w-3 h-3" />
               {currency}{archive.cost.toFixed(2)}
               {currency}{archive.cost.toFixed(2)}

+ 1 - 0
frontend/src/pages/InventoryPage.tsx

@@ -1068,6 +1068,7 @@ export default function InventoryPage() {
           isOpen={true}
           isOpen={true}
           onClose={() => setFormModal(null)}
           onClose={() => setFormModal(null)}
           spool={formModal.spool}
           spool={formModal.spool}
+          currencySymbol={currencySymbol}
         />
         />
       )}
       )}