refactor: complete module-driven architecture migration

This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -6,8 +6,8 @@ Defines the inventory module including its features, menu items,
route configurations, and self-contained module settings.
"""
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.enums import FrontendType
def _get_admin_router():
@@ -52,6 +52,50 @@ inventory_module = ModuleDefinition(
"inventory", # Vendor inventory management
],
},
# New module-driven menu definitions
menus={
FrontendType.ADMIN: [
MenuSectionDefinition(
id="vendorOps",
label_key="inventory.menu.vendor_operations",
icon="cube",
order=40,
items=[
MenuItemDefinition(
id="vendor-products",
label_key="inventory.menu.products",
icon="cube",
route="/admin/vendor-products",
order=10,
),
MenuItemDefinition(
id="inventory",
label_key="inventory.menu.inventory",
icon="archive",
route="/admin/inventory",
order=30,
),
],
),
],
FrontendType.VENDOR: [
MenuSectionDefinition(
id="products",
label_key="inventory.menu.products_inventory",
icon="package",
order=10,
items=[
MenuItemDefinition(
id="inventory",
label_key="inventory.menu.inventory",
icon="clipboard-list",
route="/vendor/{vendor_code}/inventory",
order=20,
),
],
),
],
},
is_core=False,
# =========================================================================
# Self-Contained Module Configuration

View File

@@ -1 +1,22 @@
{}
{
"inventory": {
"title": "Inventar",
"stock_level": "Lagerbestand",
"quantity": "Menge",
"reorder_point": "Nachbestellpunkt",
"adjust_stock": "Bestand anpassen",
"stock_in": "Wareneingang",
"stock_out": "Warenausgang",
"transfer": "Transfer",
"history": "Verlauf",
"low_stock_alert": "Warnung bei geringem Bestand",
"out_of_stock_alert": "Warnung bei Ausverkauf"
},
"messages": {
"stock_adjusted_successfully": "Stock adjusted successfully",
"quantity_set_successfully": "Quantity set successfully",
"inventory_entry_deleted": "Inventory entry deleted.",
"please_select_a_vendor_and_file": "Please select a vendor and file",
"import_completed_with_errors": "Import completed with errors"
}
}

View File

@@ -1 +1,31 @@
{}
{
"inventory": {
"title": "Inventory",
"stock_level": "Stock Level",
"quantity": "Quantity",
"reorder_point": "Reorder Point",
"adjust_stock": "Adjust Stock",
"stock_in": "Stock In",
"stock_out": "Stock Out",
"transfer": "Transfer",
"history": "History",
"low_stock_alert": "Low Stock Alert",
"out_of_stock_alert": "Out of Stock Alert"
},
"messages": {
"stock_adjusted_successfully": "Stock adjusted successfully",
"quantity_set_successfully": "Quantity set successfully",
"inventory_entry_deleted": "Inventory entry deleted.",
"please_select_a_vendor_and_file": "Please select a vendor and file",
"import_completed_with_errors": "Import completed with errors",
"failed_to_adjust_stock": "Failed to adjust stock.",
"failed_to_set_quantity": "Failed to set quantity.",
"failed_to_delete_entry": "Failed to delete entry.",
"import_success": "Imported {quantity} units ({created} new, {updated} updated)",
"import_completed": "Import completed: {created} created, {updated} updated, {errors} errors",
"import_failed": "Import failed",
"bulk_adjust_success": "{count} item(s) adjusted by {amount}",
"failed_to_adjust_inventory": "Failed to adjust inventory",
"exported_items": "Exported {count} item(s)"
}
}

View File

@@ -1 +1,22 @@
{}
{
"inventory": {
"title": "Inventaire",
"stock_level": "Niveau de stock",
"quantity": "Quantité",
"reorder_point": "Seuil de réapprovisionnement",
"adjust_stock": "Ajuster le stock",
"stock_in": "Entrée de stock",
"stock_out": "Sortie de stock",
"transfer": "Transfert",
"history": "Historique",
"low_stock_alert": "Alerte stock faible",
"out_of_stock_alert": "Alerte rupture de stock"
},
"messages": {
"stock_adjusted_successfully": "Stock adjusted successfully",
"quantity_set_successfully": "Quantity set successfully",
"inventory_entry_deleted": "Inventory entry deleted.",
"please_select_a_vendor_and_file": "Please select a vendor and file",
"import_completed_with_errors": "Import completed with errors"
}
}

View File

@@ -1 +1,22 @@
{}
{
"inventory": {
"title": "Inventar",
"stock_level": "Lagerniveau",
"quantity": "Quantitéit",
"reorder_point": "Nobestellungspunkt",
"adjust_stock": "Lager upaassen",
"stock_in": "Lager eran",
"stock_out": "Lager eraus",
"transfer": "Transfer",
"history": "Geschicht",
"low_stock_alert": "Niddreg Lager Alarm",
"out_of_stock_alert": "Net op Lager Alarm"
},
"messages": {
"stock_adjusted_successfully": "Stock adjusted successfully",
"quantity_set_successfully": "Quantity set successfully",
"inventory_entry_deleted": "Inventory entry deleted.",
"please_select_a_vendor_and_file": "Please select a vendor and file",
"import_completed_with_errors": "Import completed with errors"
}
}

View File

@@ -13,8 +13,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_vendor_context
from app.templates_config import templates
from models.database.user import User
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -31,7 +31,7 @@ from app.modules.inventory.schemas.inventory import (
ProductInventorySummary,
)
from app.modules.catalog.models import Product
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
logger = logging.getLogger(__name__)

View File

@@ -319,7 +319,7 @@ class InventoryTransactionService:
Returns:
Tuple of (transactions with details, total count)
"""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Build query
query = db.query(InventoryTransaction)

View File

@@ -138,6 +138,9 @@ function adminInventory() {
},
async init() {
// Load i18n translations
await I18n.loadModule('inventory');
adminInventoryLog.info('Inventory init() called');
// Guard against multiple initialization
@@ -413,12 +416,12 @@ function adminInventory() {
this.showAdjustModal = false;
this.selectedItem = null;
Utils.showToast('Stock adjusted successfully.', 'success');
Utils.showToast(I18n.t('inventory.messages.stock_adjusted_successfully'), 'success');
await this.refresh();
} catch (error) {
adminInventoryLog.error('Failed to adjust inventory:', error);
Utils.showToast(error.message || 'Failed to adjust stock.', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_stock'), 'error');
} finally {
this.saving = false;
}
@@ -444,12 +447,12 @@ function adminInventory() {
this.showSetModal = false;
this.selectedItem = null;
Utils.showToast('Quantity set successfully.', 'success');
Utils.showToast(I18n.t('inventory.messages.quantity_set_successfully'), 'success');
await this.refresh();
} catch (error) {
adminInventoryLog.error('Failed to set inventory:', error);
Utils.showToast(error.message || 'Failed to set quantity.', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_set_quantity'), 'error');
} finally {
this.saving = false;
}
@@ -470,12 +473,12 @@ function adminInventory() {
this.showDeleteModal = false;
this.selectedItem = null;
Utils.showToast('Inventory entry deleted.', 'success');
Utils.showToast(I18n.t('inventory.messages.inventory_entry_deleted'), 'success');
await this.refresh();
} catch (error) {
adminInventoryLog.error('Failed to delete inventory:', error);
Utils.showToast(error.message || 'Failed to delete entry.', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_delete_entry'), 'error');
} finally {
this.saving = false;
}
@@ -540,7 +543,7 @@ function adminInventory() {
*/
async executeImport() {
if (!this.importForm.vendor_id || !this.importForm.file) {
Utils.showToast('Please select a vendor and file', 'error');
Utils.showToast(I18n.t('inventory.messages.please_select_a_vendor_and_file'), 'error');
return;
}
@@ -559,13 +562,17 @@ function adminInventory() {
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)`,
I18n.t('inventory.messages.import_success', {
quantity: this.importResult.quantity_imported,
created: this.importResult.entries_created,
updated: this.importResult.entries_updated
}),
'success'
);
// Refresh inventory list
await this.refresh();
} else {
Utils.showToast('Import completed with errors', 'warning');
Utils.showToast(I18n.t('inventory.messages.import_completed_with_errors'), 'warning');
}
} catch (error) {
adminInventoryLog.error('Import failed:', error);
@@ -573,7 +580,7 @@ function adminInventory() {
success: false,
errors: [error.message || 'Import failed']
};
Utils.showToast(error.message || 'Import failed', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.import_failed'), 'error');
} finally {
this.importing = false;
}

View File

@@ -128,6 +128,9 @@ function vendorInventory() {
},
async init() {
// Load i18n translations
await I18n.loadModule('inventory');
vendorInventoryLog.info('Inventory init() called');
// Guard against multiple initialization
@@ -298,12 +301,12 @@ function vendorInventory() {
this.showAdjustModal = false;
this.selectedItem = null;
Utils.showToast('Stock adjusted successfully', 'success');
Utils.showToast(I18n.t('inventory.messages.stock_adjusted_successfully'), 'success');
await this.loadInventory();
} catch (error) {
vendorInventoryLog.error('Failed to adjust inventory:', error);
Utils.showToast(error.message || 'Failed to adjust stock', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_stock'), 'error');
} finally {
this.saving = false;
}
@@ -328,12 +331,12 @@ function vendorInventory() {
this.showSetModal = false;
this.selectedItem = null;
Utils.showToast('Quantity set successfully', 'success');
Utils.showToast(I18n.t('inventory.messages.quantity_set_successfully'), 'success');
await this.loadInventory();
} catch (error) {
vendorInventoryLog.error('Failed to set inventory:', error);
Utils.showToast(error.message || 'Failed to set quantity', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_set_quantity'), 'error');
} finally {
this.saving = false;
}
@@ -465,13 +468,14 @@ function vendorInventory() {
}
}
}
Utils.showToast(`${successCount} item(s) adjusted by ${this.bulkAdjustForm.quantity > 0 ? '+' : ''}${this.bulkAdjustForm.quantity}`, 'success');
const amount = this.bulkAdjustForm.quantity > 0 ? '+' + this.bulkAdjustForm.quantity : this.bulkAdjustForm.quantity;
Utils.showToast(I18n.t('inventory.messages.bulk_adjust_success', { count: successCount, amount: amount }), 'success');
this.showBulkAdjustModal = false;
this.clearSelection();
await this.loadInventory();
} catch (error) {
vendorInventoryLog.error('Bulk adjust failed:', error);
Utils.showToast(error.message || 'Failed to adjust inventory', 'error');
Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_inventory'), 'error');
} finally {
this.saving = false;
}
@@ -508,7 +512,7 @@ function vendorInventory() {
link.download = `inventory_export_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
Utils.showToast(`Exported ${selectedData.length} item(s)`, 'success');
Utils.showToast(I18n.t('inventory.messages.exported_items', { count: selectedData.length }), 'success');
}
};
}