feat: add inventory CSV import with warehouse/bin locations

- Add warehouse and bin_location columns to Inventory model
- Create inventory_import_service for bulk TSV/CSV import
- Add POST /api/v1/admin/inventory/import endpoint
- Add Import button and modal to inventory admin page
- Support both single-unit rows and explicit QUANTITY column

File format: BIN, EAN, PRODUCT (optional), QUANTITY (optional)
Products matched by GTIN/EAN, unmatched items reported.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 12:27:12 +01:00
parent d65ffa58f6
commit 63396ea6b6
6 changed files with 685 additions and 5 deletions

View File

@@ -65,6 +65,7 @@ function adminInventory() {
showAdjustModal: false,
showSetModal: false,
showDeleteModal: false,
showImportModal: false,
selectedItem: null,
// Form data
@@ -76,6 +77,17 @@ function adminInventory() {
quantity: 0
},
// Import form
importForm: {
vendor_id: '',
warehouse: 'strassen',
file: null,
clear_existing: false
},
importing: false,
importResult: null,
vendorsList: [],
// Debounce timer
searchTimeout: null,
@@ -144,6 +156,9 @@ function adminInventory() {
this.initVendorSelector();
});
// Load vendors list for import modal
await this.loadVendorsList();
// Check localStorage for saved vendor
const savedVendorId = localStorage.getItem('inventory_selected_vendor_id');
if (savedVendorId) {
@@ -501,6 +516,93 @@ function adminInventory() {
this.pagination.page = pageNum;
this.loadInventory();
}
},
// ============================================================
// Import Methods
// ============================================================
/**
* Load vendors list for import modal
*/
async loadVendorsList() {
try {
const response = await apiClient.get('/admin/vendors', { limit: 100 });
this.vendorsList = response.vendors || [];
} catch (error) {
adminInventoryLog.error('Failed to load vendors:', error);
}
},
/**
* Execute inventory import
*/
async executeImport() {
if (!this.importForm.vendor_id || !this.importForm.file) {
Utils.showToast('Please select a vendor and file', 'error');
return;
}
this.importing = true;
this.importResult = null;
try {
const formData = new FormData();
formData.append('file', this.importForm.file);
formData.append('vendor_id', this.importForm.vendor_id);
formData.append('warehouse', this.importForm.warehouse || 'strassen');
formData.append('clear_existing', this.importForm.clear_existing);
const response = await fetch('/api/v1/admin/inventory/import', {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token') || ''}`
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Import failed');
}
this.importResult = await response.json();
if (this.importResult.success) {
adminInventoryLog.info('Import successful:', this.importResult);
Utils.showToast(
`Imported ${this.importResult.quantity_imported} units (${this.importResult.entries_created} new, ${this.importResult.entries_updated} updated)`,
'success'
);
// Refresh inventory list
await this.refresh();
} else {
Utils.showToast('Import completed with errors', 'warning');
}
} catch (error) {
adminInventoryLog.error('Import failed:', error);
this.importResult = {
success: false,
errors: [error.message || 'Import failed']
};
Utils.showToast(error.message || 'Import failed', 'error');
} finally {
this.importing = false;
}
},
/**
* Close import modal and reset form
*/
closeImportModal() {
this.showImportModal = false;
this.importResult = null;
this.importForm = {
vendor_id: '',
warehouse: 'strassen',
file: null,
clear_existing: false
};
}
};
}