Vendors can now override company contact information for specific branding. Fields are nullable - if null, value is inherited from parent company. Database changes: - Add vendor.contact_email, contact_phone, website, business_address, tax_number - All nullable (null = inherit from company) - Alembic migration: 28d44d503cac Model changes: - Add effective_* properties for resolved values - Add get_contact_info_with_inheritance() helper Schema changes: - VendorCreate: Optional contact fields for override at creation - VendorUpdate: Contact fields + reset_contact_to_company flag - VendorDetailResponse: Resolved values + *_inherited flags API changes: - GET/PUT vendor endpoints return resolved contact info - PUT accepts contact overrides (empty string = reset to inherit) - _build_vendor_detail_response helper for consistent responses Service changes: - admin_service.update_vendor handles reset_contact_to_company flag - Empty strings converted to None for inheritance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.8 KiB
8.8 KiB
Vendor Contact Inheritance Migration
Overview
Feature: Add contact information fields to Vendor model with inheritance from Company.
Pattern: Nullable with Fallback - Vendor fields are nullable; if null, inherit from parent company at read time.
Benefits:
- Vendors inherit company contact info by default
- Can override specific fields for vendor-specific branding/identity
- Can reset to "inherit from company" by setting field to null
- Company updates automatically reflect in vendors that haven't overridden
Database Changes
New Columns in vendor Table
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
contact_email |
VARCHAR(255) | Yes | NULL | Override company contact email |
contact_phone |
VARCHAR(50) | Yes | NULL | Override company contact phone |
website |
VARCHAR(255) | Yes | NULL | Override company website |
business_address |
TEXT | Yes | NULL | Override company business address |
tax_number |
VARCHAR(100) | Yes | NULL | Override company tax number |
Resolution Logic
effective_value = vendor.field if vendor.field is not None else vendor.company.field
Files to Modify
1. Database Model
models/database/vendor.py- Add nullable contact fields
2. Alembic Migration
alembic/versions/xxx_add_vendor_contact_fields.py- New migration
3. Pydantic Schemas
models/schema/vendor.py:VendorUpdate- Add optional contact fieldsVendorResponse- Add resolved contact fieldsVendorDetailResponse- Add contact fields with inheritance indicator
4. Service Layer
app/services/vendor_service.py- Add contact resolution helperapp/services/admin_service.py- Update create/update to handle contact fields
5. API Endpoints
app/api/v1/admin/vendors.py- Update responses to include resolved contact info
6. Frontend
app/templates/admin/vendor-edit.html- Add contact fields with inheritance togglestatic/admin/js/vendor-edit.js- Handle inheritance UI logic
Implementation Steps
Step 1: Database Model
# models/database/vendor.py
class Vendor(Base):
# ... existing fields ...
# Contact fields (nullable = inherit from company)
contact_email = Column(String(255), nullable=True)
contact_phone = Column(String(50), nullable=True)
website = Column(String(255), nullable=True)
business_address = Column(Text, nullable=True)
tax_number = Column(String(100), nullable=True)
# Helper properties for resolved values
@property
def effective_contact_email(self) -> str | None:
return self.contact_email if self.contact_email is not None else (
self.company.contact_email if self.company else None
)
@property
def effective_contact_phone(self) -> str | None:
return self.contact_phone if self.contact_phone is not None else (
self.company.contact_phone if self.company else None
)
# ... similar for other fields ...
Step 2: Alembic Migration
def upgrade():
op.add_column('vendor', sa.Column('contact_email', sa.String(255), nullable=True))
op.add_column('vendor', sa.Column('contact_phone', sa.String(50), nullable=True))
op.add_column('vendor', sa.Column('website', sa.String(255), nullable=True))
op.add_column('vendor', sa.Column('business_address', sa.Text(), nullable=True))
op.add_column('vendor', sa.Column('tax_number', sa.String(100), nullable=True))
def downgrade():
op.drop_column('vendor', 'tax_number')
op.drop_column('vendor', 'business_address')
op.drop_column('vendor', 'website')
op.drop_column('vendor', 'contact_phone')
op.drop_column('vendor', 'contact_email')
Step 3: Pydantic Schemas
# models/schema/vendor.py
class VendorUpdate(BaseModel):
# ... existing fields ...
# Contact fields (None = don't update, empty string could mean "clear/inherit")
contact_email: str | None = None
contact_phone: str | None = None
website: str | None = None
business_address: str | None = None
tax_number: str | None = None
class VendorContactInfo(BaseModel):
"""Resolved contact information with inheritance indicators."""
contact_email: str | None
contact_phone: str | None
website: str | None
business_address: str | None
tax_number: str | None
# Inheritance flags
contact_email_inherited: bool = False
contact_phone_inherited: bool = False
website_inherited: bool = False
business_address_inherited: bool = False
tax_number_inherited: bool = False
class VendorDetailResponse(BaseModel):
# ... existing fields ...
# Resolved contact info
contact_email: str | None
contact_phone: str | None
website: str | None
business_address: str | None
tax_number: str | None
# Inheritance indicators (for UI)
contact_email_inherited: bool
contact_phone_inherited: bool
website_inherited: bool
business_address_inherited: bool
tax_number_inherited: bool
Step 4: Service Layer Helper
# app/services/vendor_service.py
def get_resolved_contact_info(self, vendor: Vendor) -> dict:
"""
Get resolved contact information with inheritance flags.
Returns dict with both values and flags indicating if inherited.
"""
company = vendor.company
return {
"contact_email": vendor.contact_email or (company.contact_email if company else None),
"contact_email_inherited": vendor.contact_email is None and company is not None,
"contact_phone": vendor.contact_phone or (company.contact_phone if company else None),
"contact_phone_inherited": vendor.contact_phone is None and company is not None,
"website": vendor.website or (company.website if company else None),
"website_inherited": vendor.website is None and company is not None,
"business_address": vendor.business_address or (company.business_address if company else None),
"business_address_inherited": vendor.business_address is None and company is not None,
"tax_number": vendor.tax_number or (company.tax_number if company else None),
"tax_number_inherited": vendor.tax_number is None and company is not None,
}
Step 5: API Endpoint Updates
# app/api/v1/admin/vendors.py
@router.get("/{vendor_identifier}", response_model=VendorDetailResponse)
def get_vendor_details(...):
vendor = vendor_service.get_vendor_by_identifier(db, vendor_identifier)
contact_info = vendor_service.get_resolved_contact_info(vendor)
return VendorDetailResponse(
# ... existing fields ...
**contact_info, # Includes values and inheritance flags
)
Step 6: Frontend UI
<!-- Vendor edit form with inheritance toggle -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Contact Email
<span x-show="contactEmailInherited" class="text-xs text-purple-500">(inherited from company)</span>
</span>
<div class="flex gap-2">
<input type="email" x-model="formData.contact_email"
:placeholder="companyContactEmail"
:class="{ 'bg-gray-100': contactEmailInherited }">
<button type="button" @click="resetToCompany('contact_email')"
x-show="!contactEmailInherited"
class="text-sm text-purple-600">
Reset to Company
</button>
</div>
</label>
API Behavior
GET /api/v1/admin/vendors/{id}
Returns resolved contact info with inheritance flags:
{
"id": 1,
"vendor_code": "VENDOR001",
"name": "My Vendor",
"contact_email": "sales@company.com",
"contact_email_inherited": true,
"contact_phone": "+352 123 456",
"contact_phone_inherited": false,
"website": "https://company.com",
"website_inherited": true
}
PUT /api/v1/admin/vendors/{id}
To override a field:
{
"contact_email": "vendor-specific@example.com"
}
To reset to inherit from company (set to null):
{
"contact_email": null
}
Testing Plan
- Create vendor - Verify contact fields are null (inheriting)
- Read vendor - Verify resolved values come from company
- Update vendor contact - Verify override works
- Reset to inherit - Verify setting null restores inheritance
- Update company - Verify change reflects in inheriting vendors
- Update company - Verify change does NOT affect overridden vendors
Rollback Plan
If issues occur:
- Run downgrade migration:
alembic downgrade -1 - Revert code changes
- Re-deploy
Progress Tracking
- Database model updated
- Alembic migration created and applied
- Pydantic schemas updated
- Service layer helper added
- API endpoints updated
- Frontend forms updated
- Tests written and passing
- Documentation updated