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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user