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:
571
app/modules/tenancy/models/vendor.py
Normal file
571
app/modules/tenancy/models/vendor.py
Normal file
@@ -0,0 +1,571 @@
|
||||
# app/modules/tenancy/models/vendor.py
|
||||
"""
|
||||
Vendor model representing entities that sell products or services.
|
||||
|
||||
This module defines the Vendor model along with its relationships to
|
||||
other models such as User (owner), Product, Customer, Order, and MarketplaceImportJob.
|
||||
"""
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# Import Base from the central database module instead of creating a new one
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class Vendor(Base, TimestampMixin):
|
||||
"""Represents a vendor in the system."""
|
||||
|
||||
__tablename__ = "vendors" # Name of the table in the database
|
||||
|
||||
id = Column(
|
||||
Integer, primary_key=True, index=True
|
||||
) # Primary key and indexed column for vendor ID
|
||||
|
||||
# Company relationship
|
||||
company_id = Column(
|
||||
Integer, ForeignKey("companies.id"), nullable=False, index=True
|
||||
) # Foreign key to the parent company
|
||||
|
||||
vendor_code = Column(
|
||||
String, unique=True, index=True, nullable=False
|
||||
) # Unique, indexed, non-nullable vendor code column
|
||||
subdomain = Column(
|
||||
String(100), unique=True, nullable=False, index=True
|
||||
) # Unique, non-nullable subdomain column with indexing
|
||||
name = Column(
|
||||
String, nullable=False
|
||||
) # Non-nullable name column for the vendor (brand name)
|
||||
description = Column(Text) # Optional text description column for the vendor
|
||||
|
||||
# Letzshop URLs - multi-language support (brand-specific marketplace feeds)
|
||||
letzshop_csv_url_fr = Column(String) # URL for French CSV in Letzshop
|
||||
letzshop_csv_url_en = Column(String) # URL for English CSV in Letzshop
|
||||
letzshop_csv_url_de = Column(String) # URL for German CSV in Letzshop
|
||||
|
||||
# Letzshop Vendor Identity (for linking to Letzshop marketplace profile)
|
||||
letzshop_vendor_id = Column(
|
||||
String(100), unique=True, nullable=True, index=True
|
||||
) # Letzshop's vendor identifier
|
||||
letzshop_vendor_slug = Column(
|
||||
String(200), nullable=True, index=True
|
||||
) # Letzshop shop URL slug (e.g., "my-shop" from letzshop.lu/vendors/my-shop)
|
||||
|
||||
# ========================================================================
|
||||
# Letzshop Feed Settings (atalanda namespace)
|
||||
# ========================================================================
|
||||
# These are default values applied to all products in the Letzshop feed
|
||||
# See https://letzshop.lu/en/dev#google_csv for documentation
|
||||
|
||||
# Default VAT rate for new products: 0 (exempt), 3 (super-reduced), 8 (reduced), 14 (intermediate), 17 (standard)
|
||||
letzshop_default_tax_rate = Column(Integer, default=17, nullable=False)
|
||||
|
||||
# Product sort priority on Letzshop (0.0-10.0, higher = displayed first)
|
||||
# Note: Having all products rated above 7 is not permitted by Letzshop
|
||||
letzshop_boost_sort = Column(String(10), default="5.0") # Stored as string for precision
|
||||
|
||||
# Delivery method: 'nationwide', 'package_delivery', 'self_collect' (comma-separated for multiple)
|
||||
# 'nationwide' automatically includes package_delivery and self_collect
|
||||
letzshop_delivery_method = Column(String(100), default="package_delivery")
|
||||
|
||||
# Pre-order days: number of days before item ships (default 1 day)
|
||||
letzshop_preorder_days = Column(Integer, default=1)
|
||||
|
||||
# Status (vendor-specific, can differ from company status)
|
||||
is_active = Column(
|
||||
Boolean, default=True
|
||||
) # Boolean to indicate if the vendor brand is active
|
||||
is_verified = Column(
|
||||
Boolean, default=False
|
||||
) # Boolean to indicate if the vendor brand is verified
|
||||
|
||||
# ========================================================================
|
||||
# Contact Information (nullable = inherit from company)
|
||||
# ========================================================================
|
||||
# These fields allow vendor-specific branding/identity.
|
||||
# If null, the value is inherited from the parent company.
|
||||
contact_email = Column(String(255), nullable=True) # Override company contact email
|
||||
contact_phone = Column(String(50), nullable=True) # Override company contact phone
|
||||
website = Column(String(255), nullable=True) # Override company website
|
||||
business_address = Column(Text, nullable=True) # Override company business address
|
||||
tax_number = Column(String(100), nullable=True) # Override company tax number
|
||||
|
||||
# ========================================================================
|
||||
# Language Settings
|
||||
# ========================================================================
|
||||
# Supported languages: en, fr, de, lb (Luxembourgish)
|
||||
default_language = Column(
|
||||
String(5), nullable=False, default="fr"
|
||||
) # Default language for vendor content (products, emails, etc.)
|
||||
dashboard_language = Column(
|
||||
String(5), nullable=False, default="fr"
|
||||
) # Language for vendor team dashboard UI
|
||||
storefront_language = Column(
|
||||
String(5), nullable=False, default="fr"
|
||||
) # Default language for customer-facing storefront
|
||||
storefront_languages = Column(
|
||||
JSON, nullable=False, default=["fr", "de", "en"]
|
||||
) # Array of enabled languages for storefront language selector
|
||||
|
||||
# Currency/number formatting locale (e.g., 'fr-LU' = "29,99 EUR", 'en-GB' = "EUR29.99")
|
||||
# NULL means inherit from platform default (AdminSetting 'default_storefront_locale')
|
||||
storefront_locale = Column(String(10), nullable=True)
|
||||
|
||||
# ========================================================================
|
||||
# Relationships
|
||||
# ========================================================================
|
||||
company = relationship(
|
||||
"Company", back_populates="vendors"
|
||||
) # Relationship with Company model for the parent company
|
||||
vendor_users = relationship(
|
||||
"VendorUser", back_populates="vendor"
|
||||
) # Relationship with VendorUser model for users in this vendor
|
||||
products = relationship(
|
||||
"Product", back_populates="vendor"
|
||||
) # Relationship with Product model for products of this vendor
|
||||
customers = relationship(
|
||||
"Customer", back_populates="vendor"
|
||||
) # Relationship with Customer model for customers of this vendor
|
||||
orders = relationship(
|
||||
"Order", back_populates="vendor"
|
||||
) # Relationship with Order model for orders placed by this vendor
|
||||
marketplace_import_jobs = relationship(
|
||||
"MarketplaceImportJob", back_populates="vendor"
|
||||
) # Relationship with MarketplaceImportJob model for import jobs related to this vendor
|
||||
|
||||
# Letzshop integration credentials (one-to-one)
|
||||
letzshop_credentials = relationship(
|
||||
"VendorLetzshopCredentials",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Invoice settings (one-to-one)
|
||||
invoice_settings = relationship(
|
||||
"VendorInvoiceSettings",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Invoices (one-to-many)
|
||||
invoices = relationship(
|
||||
"Invoice",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Email template overrides (one-to-many)
|
||||
email_templates = relationship(
|
||||
"VendorEmailTemplate",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Email settings (one-to-one) - vendor SMTP/provider configuration
|
||||
email_settings = relationship(
|
||||
"VendorEmailSettings",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Subscription (one-to-one)
|
||||
subscription = relationship(
|
||||
"VendorSubscription",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Add-ons purchased by vendor (one-to-many)
|
||||
addons = relationship(
|
||||
"VendorAddOn",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Billing/invoice history (one-to-many)
|
||||
billing_history = relationship(
|
||||
"BillingHistory",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="BillingHistory.invoice_date.desc()",
|
||||
)
|
||||
|
||||
domains = relationship(
|
||||
"VendorDomain",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="VendorDomain.is_primary.desc()",
|
||||
) # Relationship with VendorDomain model for custom domains of the vendor
|
||||
|
||||
# Single theme relationship (ONE vendor = ONE theme)
|
||||
# A vendor has ONE active theme stored in the vendor_themes table.
|
||||
# Theme presets available: default, modern, classic, minimal, vibrant
|
||||
vendor_theme = relationship(
|
||||
"VendorTheme",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
) # Relationship with VendorTheme model for the active theme of the vendor
|
||||
|
||||
# Content pages relationship (vendor can override platform default pages)
|
||||
content_pages = relationship(
|
||||
"ContentPage", back_populates="vendor", cascade="all, delete-orphan"
|
||||
) # Relationship with ContentPage model for vendor-specific content pages
|
||||
|
||||
# Onboarding progress (one-to-one)
|
||||
onboarding = relationship(
|
||||
"VendorOnboarding",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Media library (one-to-many)
|
||||
media_files = relationship(
|
||||
"MediaFile",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Platform memberships (many-to-many via junction table)
|
||||
vendor_platforms = relationship(
|
||||
"VendorPlatform",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Loyalty program (one-to-one)
|
||||
loyalty_program = relationship(
|
||||
"LoyaltyProgram",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the Vendor object."""
|
||||
return f"<Vendor(id={self.id}, vendor_code='{self.vendor_code}', name='{self.name}', subdomain='{self.subdomain}')>"
|
||||
|
||||
# ========================================================================
|
||||
# Theme Helper Methods to get active theme and other related information
|
||||
# ========================================================================
|
||||
|
||||
def get_effective_theme(self) -> dict:
|
||||
"""
|
||||
Get active theme for this vendor.
|
||||
|
||||
Returns theme from vendor_themes table, or default theme if not set.
|
||||
|
||||
Returns:
|
||||
dict: Theme configuration with colors, fonts, layout, etc.
|
||||
"""
|
||||
# Check vendor_themes table
|
||||
if self.vendor_theme and self.vendor_theme.is_active:
|
||||
return self.vendor_theme.to_dict()
|
||||
|
||||
# Return default theme
|
||||
return self._get_default_theme()
|
||||
|
||||
def _get_default_theme(self) -> dict:
|
||||
"""Return the default theme configuration."""
|
||||
return {
|
||||
"theme_name": "default",
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"border": "#e5e7eb",
|
||||
},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"branding": {
|
||||
"logo": None,
|
||||
"logo_dark": None,
|
||||
"favicon": None,
|
||||
"banner": None,
|
||||
},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
"social_links": {},
|
||||
"custom_css": None,
|
||||
"css_variables": {
|
||||
"--color-primary": "#6366f1",
|
||||
"--color-secondary": "#8b5cf6",
|
||||
"--color-accent": "#ec4899",
|
||||
"--color-background": "#ffffff",
|
||||
"--color-text": "#1f2937",
|
||||
"--color-border": "#e5e7eb",
|
||||
"--font-heading": "Inter, sans-serif",
|
||||
"--font-body": "Inter, sans-serif",
|
||||
},
|
||||
}
|
||||
|
||||
def get_primary_color(self) -> str:
|
||||
"""Get primary color from active theme."""
|
||||
theme = self.get_effective_theme()
|
||||
return theme.get("colors", {}).get(
|
||||
"primary", "#6366f1"
|
||||
) # Default to default theme if not found
|
||||
|
||||
def get_logo_url(self) -> str:
|
||||
"""Get logo URL from active theme."""
|
||||
theme = self.get_effective_theme()
|
||||
return theme.get("branding", {}).get(
|
||||
"logo"
|
||||
) # Return None or the logo URL if found
|
||||
|
||||
# ========================================================================
|
||||
# Domain Helper Methods
|
||||
# ========================================================================
|
||||
|
||||
@property
|
||||
def primary_domain(self):
|
||||
"""Get the primary custom domain for this vendor."""
|
||||
for domain in self.domains:
|
||||
if domain.is_primary and domain.is_active:
|
||||
return domain.domain # Return the domain if it's primary and active
|
||||
return None
|
||||
|
||||
@property
|
||||
def all_domains(self):
|
||||
"""Get all active domains (subdomain + custom domains)."""
|
||||
domains = [
|
||||
f"{self.subdomain}.{settings.platform_domain}"
|
||||
] # Start with the main subdomain
|
||||
for domain in self.domains:
|
||||
if domain.is_active:
|
||||
domains.append(domain.domain) # Add other active custom domains
|
||||
return domains
|
||||
|
||||
# ========================================================================
|
||||
# Contact Resolution Helper Properties
|
||||
# ========================================================================
|
||||
# These properties return the effective value (vendor override or company fallback)
|
||||
|
||||
@property
|
||||
def effective_contact_email(self) -> str | None:
|
||||
"""Get contact email (vendor override or company fallback)."""
|
||||
if self.contact_email is not None:
|
||||
return self.contact_email
|
||||
return self.company.contact_email if self.company else None
|
||||
|
||||
@property
|
||||
def effective_contact_phone(self) -> str | None:
|
||||
"""Get contact phone (vendor override or company fallback)."""
|
||||
if self.contact_phone is not None:
|
||||
return self.contact_phone
|
||||
return self.company.contact_phone if self.company else None
|
||||
|
||||
@property
|
||||
def effective_website(self) -> str | None:
|
||||
"""Get website (vendor override or company fallback)."""
|
||||
if self.website is not None:
|
||||
return self.website
|
||||
return self.company.website if self.company else None
|
||||
|
||||
@property
|
||||
def effective_business_address(self) -> str | None:
|
||||
"""Get business address (vendor override or company fallback)."""
|
||||
if self.business_address is not None:
|
||||
return self.business_address
|
||||
return self.company.business_address if self.company else None
|
||||
|
||||
@property
|
||||
def effective_tax_number(self) -> str | None:
|
||||
"""Get tax number (vendor override or company fallback)."""
|
||||
if self.tax_number is not None:
|
||||
return self.tax_number
|
||||
return self.company.tax_number if self.company else None
|
||||
|
||||
def get_contact_info_with_inheritance(self) -> dict:
|
||||
"""
|
||||
Get all contact info with inheritance flags.
|
||||
|
||||
Returns dict with resolved values and flags indicating if inherited from company.
|
||||
"""
|
||||
company = self.company
|
||||
return {
|
||||
"contact_email": self.effective_contact_email,
|
||||
"contact_email_inherited": self.contact_email is None
|
||||
and company is not None,
|
||||
"contact_phone": self.effective_contact_phone,
|
||||
"contact_phone_inherited": self.contact_phone is None
|
||||
and company is not None,
|
||||
"website": self.effective_website,
|
||||
"website_inherited": self.website is None and company is not None,
|
||||
"business_address": self.effective_business_address,
|
||||
"business_address_inherited": self.business_address is None
|
||||
and company is not None,
|
||||
"tax_number": self.effective_tax_number,
|
||||
"tax_number_inherited": self.tax_number is None and company is not None,
|
||||
}
|
||||
|
||||
|
||||
class VendorUserType(str, enum.Enum):
|
||||
"""Types of vendor users."""
|
||||
|
||||
OWNER = "owner" # Vendor owner (full access to vendor area)
|
||||
TEAM_MEMBER = "member" # Team member (role-based access to vendor area)
|
||||
|
||||
|
||||
class VendorUser(Base, TimestampMixin):
|
||||
"""
|
||||
Represents a user's membership in a vendor.
|
||||
|
||||
- Owner: Created automatically when vendor is created
|
||||
- Team Member: Invited by owner via email
|
||||
"""
|
||||
|
||||
__tablename__ = "vendor_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
"""Unique identifier for each VendorUser entry."""
|
||||
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
"""Foreign key linking to the associated Vendor."""
|
||||
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
"""Foreign key linking to the associated User."""
|
||||
|
||||
# Distinguish between owner and team member
|
||||
user_type = Column(String, nullable=False, default=VendorUserType.TEAM_MEMBER.value)
|
||||
|
||||
# Role for team members (NULL for owners - they have all permissions)
|
||||
role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
|
||||
"""Foreign key linking to the associated Role."""
|
||||
|
||||
invited_by = Column(Integer, ForeignKey("users.id"))
|
||||
"""Foreign key linking to the user who invited this VendorUser."""
|
||||
invitation_token = Column(String, nullable=True, index=True) # For email activation
|
||||
invitation_sent_at = Column(DateTime, nullable=True)
|
||||
invitation_accepted_at = Column(DateTime, nullable=True)
|
||||
|
||||
is_active = Column(
|
||||
Boolean, default=False, nullable=False
|
||||
) # False until invitation accepted
|
||||
"""Indicates whether the VendorUser role is active."""
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="vendor_users")
|
||||
"""Relationship to the Vendor model, representing the associated vendor."""
|
||||
|
||||
user = relationship(
|
||||
"User", foreign_keys=[user_id], back_populates="vendor_memberships"
|
||||
)
|
||||
"""Relationship to the User model, representing the user who holds this role within the vendor."""
|
||||
|
||||
inviter = relationship("User", foreign_keys=[invited_by])
|
||||
"""Optional relationship to the User model, representing the user who invited this VendorUser."""
|
||||
|
||||
role = relationship("Role", back_populates="vendor_users")
|
||||
"""Relationship to the Role model, representing the role held by the vendor user."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the VendorUser instance.
|
||||
|
||||
Returns:
|
||||
str: A string that includes the vendor_id, the user_id and the user_type of the VendorUser instance.
|
||||
"""
|
||||
return f"<VendorUser(vendor_id={self.vendor_id}, user_id={self.user_id}, type={self.user_type})>"
|
||||
|
||||
@property
|
||||
def is_owner(self) -> bool:
|
||||
"""Check if this is an owner membership."""
|
||||
return self.user_type == VendorUserType.OWNER.value
|
||||
|
||||
@property
|
||||
def is_team_member(self) -> bool:
|
||||
"""Check if this is a team member (not owner)."""
|
||||
return self.user_type == VendorUserType.TEAM_MEMBER.value
|
||||
|
||||
@property
|
||||
def is_invitation_pending(self) -> bool:
|
||||
"""Check if invitation is still pending."""
|
||||
return self.invitation_token is not None and self.invitation_accepted_at is None
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""
|
||||
Check if user has a specific permission.
|
||||
|
||||
Owners always have all permissions.
|
||||
Team members check their role's permissions.
|
||||
"""
|
||||
# Owners have all permissions
|
||||
if self.is_owner:
|
||||
return True
|
||||
|
||||
# Inactive users have no permissions
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check role permissions
|
||||
if self.role and self.role.permissions:
|
||||
return permission in self.role.permissions
|
||||
|
||||
return False
|
||||
|
||||
def get_all_permissions(self) -> list:
|
||||
"""Get all permissions this user has."""
|
||||
if self.is_owner:
|
||||
# Return all possible permissions
|
||||
from app.core.permissions import VendorPermissions
|
||||
|
||||
return list(VendorPermissions.__members__.values())
|
||||
|
||||
if self.role and self.role.permissions:
|
||||
return self.role.permissions
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class Role(Base, TimestampMixin):
|
||||
"""Represents a role within a vendor's system."""
|
||||
|
||||
__tablename__ = "roles" # Name of the table in the database
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
"""Unique identifier for each Role entry."""
|
||||
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
"""Foreign key linking to the associated Vendor."""
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
"""Name of the role, with a maximum length of 100 characters."""
|
||||
|
||||
permissions = Column(JSON, default=list)
|
||||
"""Permissions assigned to this role, stored as a JSON array."""
|
||||
|
||||
vendor = relationship("Vendor")
|
||||
"""Relationship to the Vendor model, representing the associated vendor."""
|
||||
|
||||
vendor_users = relationship("VendorUser", back_populates="role")
|
||||
"""Back-relationship to the VendorUser model, representing users with this role."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the Role instance.
|
||||
|
||||
Returns:
|
||||
str: A string that includes the id and name of the Role instance.
|
||||
"""
|
||||
return f"<Role(id={self.id}, name='{self.name}', vendor_id={self.vendor_id})>"
|
||||
|
||||
|
||||
__all__ = ["Vendor", "VendorUser", "VendorUserType", "Role"]
|
||||
Reference in New Issue
Block a user