# models.py from django.db import models from django.core.exceptions import ValidationError # Import settings to reference the AUTH_USER_MODEL from django.conf import settings import re # For custom validation if needed # It's good practice to define choices as constants ADDRESS_TYPES = [ ('billing', 'Billing'), ('shipping', 'Shipping'), ('physical', 'Physical'), # Add other types as needed ] # USER_TYPES constant removed as we now have separate models EMAIL_RECIPIENT_TYPES = [ ('to', 'To'), ('cc', 'Cc'), ('from', 'From'), # Added 'from' for consistency # ('bcc', 'Bcc'), # Usually not stored directly ] # --- Base Models (like Currency, Country, State first as others depend on them) --- class Currency(models.Model): name = models.CharField(max_length=100, unique=True) code = models.CharField(max_length=3, unique=True, help_text="ISO 4217 currency code (e.g., AUD, USD)") symbol = models.CharField(max_length=5, blank=True, null=True) class Meta: db_table = 'currencies' # CakePHP convention ordering = ['name'] verbose_name_plural = "Currencies" # Correct pluralization def __str__(self): return f"{self.name} ({self.code})" class Country(models.Model): name = models.CharField(max_length=100, unique=True) # code = models.CharField(max_length=2, unique=True, help_text="ISO 3166-1 alpha-2 code (e.g., AU, US)") currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='countries', null=True, blank=True) class Meta: db_table = 'countries' # CakePHP convention ordering = ['name'] verbose_name_plural = "Countries" # Correct pluralization def __str__(self): return self.name class State(models.Model): name = models.CharField(max_length=100) shortform = models.CharField(max_length=10, unique=True, help_text="Abbreviation (e.g., NSW, VIC, CA)") # country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='states') # Added link to country class Meta: db_table = 'states' # CakePHP convention ordering = ['name'] def __str__(self): return f"{self.name}" class Status(models.Model): name = models.CharField(max_length=100, unique=True) # 'class' is a reserved keyword in Python, rename the field css_class = models.CharField(max_length=50, blank=True, null=True, help_text="CSS class for styling", db_column="class") class Meta: db_table = 'statuses' # CakePHP convention ordering = ['name'] verbose_name_plural = "Statuses" def __str__(self): return self.name # Helper methods from CakePHP model can be added here or as manager methods if needed # def get_json(self): ... # def get_class_names_json(self): ... class CustomerCategory(models.Model): name = models.CharField(max_length=100, unique=True) class Meta: db_table = 'customer_categories' # CakePHP convention ordering = ['name'] verbose_name_plural = "Customer Categories" def __str__(self): return self.name class ContactCategory(models.Model): name = models.CharField(max_length=100, unique=True) class Meta: db_table = 'contact_categories' # CakePHP convention ordering = ['name'] verbose_name_plural = "Contact Categories" def __str__(self): return self.name class Industry(models.Model): name = models.CharField(max_length=150) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='sub_industries') class Meta: db_table = 'industries' # CakePHP convention ordering = ['name'] verbose_name_plural = "Industries" def __str__(self): if self.parent: return f"{self.parent} -> {self.name}" return self.name class ProductCategory(models.Model): name = models.CharField(max_length=100, unique=True) class Meta: db_table = 'product_categories' # CakePHP convention ordering = ['name'] verbose_name_plural = "Product Categories" def __str__(self): return self.name class FreightForwarder(models.Model): name = models.CharField(max_length=150, unique=True) class Meta: db_table = 'freight_forwarders' # CakePHP convention ordering = ['name'] def __str__(self): return self.name class FreightService(models.Model): name = models.CharField(max_length=100) freight_forwarder = models.ForeignKey(FreightForwarder, on_delete=models.CASCADE, related_name='freight_services') class Meta: db_table = 'freight_services' # CakePHP convention ordering = ['freight_forwarder__name', 'name'] unique_together = ('name', 'freight_forwarder') def __str__(self): return f"{self.freight_forwarder.name} - {self.name}" # --- Core Models --- class Customer(models.Model): name = models.CharField(max_length=200, unique=True) abn = models.CharField(max_length=20, blank=True, null=True, help_text="Australian Business Number") customer_category = models.ForeignKey(CustomerCategory, on_delete=models.SET_NULL, null=True, blank=True, related_name='customers') country = models.ForeignKey(Country, on_delete=models.PROTECT, related_name='customers') # Assuming country is mandatory payment_terms = models.CharField(max_length=100, blank=True, null=True) # CakePHP HABTM industries = models.ManyToManyField(Industry, related_name='customers', blank=True, db_table='customers_industries') # Explicit join table created = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'customers' # CakePHP convention ordering = ['name'] def __str__(self): return self.name def clean(self): # Example implementation of checkABN validation if self.abn: numbers = re.sub(r"\s*\D*", "", self.abn) if len(numbers) != 11: raise ValidationError({'abn': 'ABN must contain exactly 11 digits.'}) super().clean() class Address(models.Model): customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='addresses') address_line_1 = models.CharField(max_length=255, verbose_name="Address") address_line_2 = models.CharField(max_length=255, blank=True, null=True) city = models.CharField(max_length=100) postcode = models.CharField(max_length=20) state = models.ForeignKey(State, on_delete=models.PROTECT, related_name='addresses') # Protect states from deletion if used country = models.ForeignKey(Country, on_delete=models.PROTECT, related_name='addresses') # Protect countries type = models.CharField(max_length=20, choices=ADDRESS_TYPES) # Use choices for type class Meta: db_table = 'addresses' # CakePHP convention ordering = ['customer__name', 'type'] verbose_name_plural = "Addresses" def __str__(self): return f"{self.customer.name} - {self.get_type_display()} Address" def clean(self): # Implementation for checkStates validation # Assumes Australia Country ID is 1 and Overseas State ID is 9 (adapt IDs if different) # It's better to check by country code or name if IDs might change is_australia = self.country.code == 'AU' # Example check by code # Need a way to identify the 'Overseas' state reliably (e.g., a flag on the State model) # is_overseas_state = self.state.is_overseas # Assuming an 'is_overseas' boolean field exists on State # Placeholder logic - replace with reliable check is_overseas_state_placeholder = self.state_id == 9 if is_australia and is_overseas_state_placeholder: raise ValidationError("Australian addresses cannot use the 'Overseas' state.") if not is_australia and not is_overseas_state_placeholder: raise ValidationError("Non-Australian addresses must use the 'Overseas' state.") super().clean() class Principle(models.Model): name = models.CharField(max_length=200, unique=True) city = models.CharField(max_length=100, blank=True, null=True) country = models.ForeignKey(Country, on_delete=models.PROTECT, related_name='principles') currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='principles') class Meta: db_table = 'principles' # CakePHP convention ordering = ['name'] def __str__(self): return self.name # getCityCountryList can be a manager method or utility function class PrincipleAddress(models.Model): principle = models.ForeignKey(Principle, on_delete=models.CASCADE, related_name='addresses') address_line_1 = models.CharField(max_length=255, verbose_name="Address") address_line_2 = models.CharField(max_length=255, blank=True, null=True) city = models.CharField(max_length=100) postcode = models.CharField(max_length=20) # Assuming state is not needed here based on Cake model, but could be added country = models.ForeignKey(Country, on_delete=models.PROTECT, related_name='principle_addresses') type = models.CharField(max_length=50, blank=True, null=True, help_text="e.g., Factory, Office") # Example type class Meta: db_table = 'principle_addresses' # CakePHP convention ordering = ['principle__name'] verbose_name_plural = "Principle Addresses" def __str__(self): return f"Address for {self.principle.name}" # --- NEW Contact Model (for external contacts) --- class Contact(models.Model): """Represents an external contact person associated with a Customer or Principle.""" first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100, blank=True, null=True) email = models.EmailField(unique=True) # Assuming unique email for contacts job_title = models.CharField(max_length=100, blank=True, null=True) # Relationships #principle = models.ForeignKey(Principle, on_delete=models.SET_NULL, null=True, blank=True, related_name='contacts') customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name='contacts') contact_category = models.ForeignKey(ContactCategory, on_delete=models.SET_NULL, null=True, blank=True, related_name='contacts') # Field from Vault script - indicates if created_at automatically # by_vault = models.BooleanField(default=False) #created_at = models.DateTimeField(auto_now_add=True) #updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'contacts' # CakePHP convention (assuming this replaces the old 'users' for external contacts) ordering = ['first_name', 'last_name'] verbose_name = "External Contact" verbose_name_plural = "External Contacts" def __str__(self): company = "" if self.customer: company = f" ({self.customer.name})" #elif self.principle: # company = f" ({self.principle.name})" return f"{self.first_name} {self.last_name or ''}{company} - {self.email}" @property def full_name(self): return f"{self.first_name} {self.last_name or ''}".strip() # --- Product Related Models --- class Product(models.Model): principle = models.ForeignKey(Principle, on_delete=models.CASCADE, related_name='products') product_category = models.ForeignKey(ProductCategory, on_delete=models.SET_NULL, null=True, blank=True, related_name='products') name = models.CharField(max_length=200) description = models.TextField(blank=True, null=True) model_number = models.CharField(max_length=100, blank=True, null=True) # Assuming a model number field exists class Meta: db_table = 'products' # CakePHP convention ordering = ['principle__name', 'name'] unique_together = ('principle', 'name') # Or model_number? def __str__(self): return f"{self.name} ({self.principle.name})" class ProductAttachment(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='attachments') # Use FileField for uploads. Requires MEDIA_ROOT/MEDIA_URL setup. file = models.FileField(upload_to='product_attachments/') description = models.CharField(max_length=255, blank=True, null=True) uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'product_attachments' # CakePHP convention def __str__(self): return f"Attachment for {self.product.name}" class ProductOptionsCategory(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='option_categories') name = models.CharField(max_length=100) location = models.PositiveIntegerField(null=True, blank=True, help_text="Ordering position") exclusive = models.BooleanField(default=False, help_text="Can only one option in this category be chosen?") class Meta: db_table = 'product_options_categories' # CakePHP convention ordering = ['product__name', 'location', 'name'] verbose_name_plural = "Product Option Categories" def __str__(self): return f"{self.name} (Options for {self.product.name})" class ProductOption(models.Model): product_options_category = models.ForeignKey(ProductOptionsCategory, on_delete=models.CASCADE, related_name='options') name = models.CharField(max_length=150) value = models.CharField(max_length=255, blank=True, null=True, help_text="Value associated with the option, if any") price_adjustment = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) # Example field is_default = models.BooleanField(default=False) class Meta: db_table = 'product_options' # CakePHP convention ordering = ['product_options_category', 'name'] def __str__(self): return f"{self.name} ({self.product_options_category.name})" def save(self, *args, **kwargs): # Logic to enforce only one default option per category if self.is_default: ProductOption.objects.filter( product_options_category=self.product_options_category, is_default=True ).exclude(pk=self.pk).update(is_default=False) super().save(*args, **kwargs) # --- Document / Enquiry / Job / Order Flow --- class Document(models.Model): # Represents the base for Quotes, Invoices, POs etc. DOC_TYPES = [ ('quote', 'Quotation'), ('invoice', 'Invoice'), ('purchaseOrder', 'Purchase Order'), ('orderAck', 'Order Acknowledgement'), ('packingList', 'Packing List'), ] type = models.CharField(max_length=20, choices=DOC_TYPES) # Link to the internal user who created_at/owns this document user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, # Keep document if user is deleted? Or PROTECT? null=True, blank=True, related_name='created_at_documents', help_text="Internal user who created_at this document" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Counter cache fields need manual implementation # doc_page_count = models.PositiveIntegerField(default=0) class Meta: db_table = 'documents' # CakePHP convention def __str__(self): # Try to get the title from the related specific document type related_doc = None if self.type == 'quote' and hasattr(self, 'quote'): related_doc = self.quote elif self.type == 'invoice' and hasattr(self, 'invoice'): related_doc = self.invoice elif self.type == 'purchaseOrder' and hasattr(self, 'purchaseorder'): related_doc = self.purchaseorder elif self.type == 'orderAck' and hasattr(self, 'orderacknowledgement'): related_doc = self.orderacknowledgement elif self.type == 'packingList' and hasattr(self, 'packinglist'): related_doc = self.packinglist title = getattr(related_doc, 'title', f"Document {self.id}") if related_doc else f"Document {self.id}" return f"{self.get_type_display()} - {title}" # Methods like getCurrency, getDocType, getEnquiry etc. can be added here class DocPage(models.Model): document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name='pages') page_number = models.PositiveIntegerField() content = models.TextField(blank=True, null=True) # Or maybe link to a scanned image FileField? class Meta: db_table = 'doc_pages' # CakePHP convention ordering = ['document', 'page_number'] unique_together = ('document', 'page_number') def __str__(self): return f"Page {self.page_number} of Document {self.document.id}" class Attachment(models.Model): # General attachment model, linked via DocumentAttachment principle = models.ForeignKey(Principle, on_delete=models.CASCADE, related_name='attachments', null=True, blank=True) # If attachments can belong to principles name = models.CharField(max_length=255) # Original filename or description type = models.CharField(max_length=100) # MIME type size = models.PositiveIntegerField() file = models.FileField(upload_to='general_attachments/') # Actual file storage archived = models.BooleanField(default=False) uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'attachments' # CakePHP convention def __str__(self): return self.name class DocumentAttachment(models.Model): document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name='doc_attachments') # Changed related_name to avoid clash attachment = models.ForeignKey(Attachment, on_delete=models.CASCADE, related_name='document_links') # Link to the generic attachment class Meta: db_table = 'document_attachments' # Match CakePHP table name (already set) unique_together = ('document', 'attachment') def __str__(self): return f"Attachment {self.attachment.name} linked to Document {self.document.id}" class Enquiry(models.Model): title = models.CharField(max_length=255, unique=True, help_text="Unique Enquiry reference (e.g., CMC...E...)") # Link to the internal user who owns/created_at this enquiry user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, # Protect user from deletion if they have enquiries? related_name='created_at_enquiries', help_text="Internal user who owns/created_at this enquiry" ) customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='enquiries') # Link to the external contact person for this enquiry contact = models.ForeignKey( # Renamed from contact_user Contact, on_delete=models.PROTECT, # Protect contact if they are linked? Or SET_NULL? related_name='contact_enquiries', help_text="External contact person for this enquiry" ) state = models.ForeignKey(State, on_delete=models.PROTECT, related_name='enquiries') country = models.ForeignKey(Country, on_delete=models.PROTECT, related_name='enquiries') principle = models.ForeignKey(Principle, on_delete=models.PROTECT, related_name='enquiries') status = models.ForeignKey(Status, on_delete=models.PROTECT, related_name='enquiries') billing_address = models.ForeignKey(Address, on_delete=models.SET_NULL, null=True, blank=True, related_name='billing_enquiries') shipping_address = models.ForeignKey(Address, on_delete=models.SET_NULL, null=True, blank=True, related_name='shipping_enquiries') gst = models.BooleanField(default=True, help_text="Does GST apply to this enquiry?") comments = models.TextField(blank=True, null=True) created = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # counterCache fields need manual implementation # quote_count = models.PositiveIntegerField(default=0) # invoice_count = models.PositiveIntegerField(default=0) # job_count = models.PositiveIntegerField(default=0) # orderacknowledgement_count = models.PositiveIntegerField(default=0) # packinglist_count = models.PositiveIntegerField(default=0) class Meta: db_table = 'enquiries' # CakePHP convention ordering = ['-created'] verbose_name_plural = "Enquiries" def __str__(self): return self.title def clean(self): # Implementation for checkStates validation (similar to Address) is_australia = self.country.code == 'AU' # Example check by code # Placeholder logic - replace with reliable check is_overseas_state_placeholder = self.state_id == 9 if is_australia and is_overseas_state_placeholder: raise ValidationError("Australian enquiries cannot use the 'Overseas' state.") if not is_australia and not is_overseas_state_placeholder: raise ValidationError("Non-Australian enquiries must use the 'Overseas' state.") super().clean() # formatAddress can be a model method or moved to templates/views class EnquiryEmailQueue(models.Model): enquiry = models.ForeignKey(Enquiry, on_delete=models.CASCADE, related_name='email_queue') email_subject = models.CharField(max_length=255) email_body = models.TextField() recipient_email = models.EmailField() sent = models.BooleanField(default=False) send_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'enquiry_email_queues' # CakePHP convention def __str__(self): return f"Email for Enquiry {self.enquiry.title} to {self.recipient_email}" class EnquiryFile(models.Model): enquiry = models.ForeignKey(Enquiry, on_delete=models.CASCADE, related_name='files') file = models.FileField(upload_to='enquiry_files/') description = models.CharField(max_length=255, blank=True, null=True) uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'enquiry_files' # CakePHP convention def __str__(self): return f"File for Enquiry {self.enquiry.title}" class Job(models.Model): title = models.CharField(max_length=100, unique=True, help_text="Unique Job reference (e.g., JAN24...J...)") enquiry = models.ForeignKey(Enquiry, on_delete=models.SET_NULL, null=True, blank=True, related_name='jobs') # Job might exist without initial enquiry? customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='jobs') # Link to the external contact person for this job contact = models.ForeignKey( Contact, on_delete=models.SET_NULL, # Allow job to exist if contact is deleted? null=True, blank=True, related_name='contact_jobs', help_text="External contact person for this job" ) state = models.ForeignKey(State, on_delete=models.PROTECT, related_name='jobs') # State where job is located? currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='jobs') # Currency of the job value? customer_order_number = models.CharField(max_length=100, blank=True, null=True) date_order_received = models.DateField(null=True, blank=True) # Define choices for categories if applicable # domestic_freight_paid_by = models.CharField(max_length=50, blank=True, null=True) # sale_category = models.CharField(max_length=50, blank=True, null=True) # shipment_category = models.CharField(max_length=50, blank=True, null=True) all_sent = models.BooleanField(default=False) all_paid = models.BooleanField(default=False) comments = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'jobs' # CakePHP convention ordering = ['-date_order_received', 'title'] def __str__(self): return self.title # newJob logic should be moved to a manager method or a service/utility function class LineItem(models.Model): document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name='line_items') product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='line_items', null=True, blank=True) # Protect product from deletion if used item_number = models.PositiveIntegerField() description = models.TextField(blank=True, null=True) # Allow manual description quantity = models.DecimalField(max_digits=10, decimal_places=2) # Use Decimal for quantity if needed unit_price = models.DecimalField(max_digits=12, decimal_places=2) # Add fields like discount, tax rate if needed class Meta: db_table = 'line_items' # CakePHP convention ordering = ['document', 'item_number'] unique_together = ('document', 'item_number') def __str__(self): return f"Item {self.item_number} for Doc {self.document.id}" @property def total_price(self): return self.quantity * self.unit_price class Costing(models.Model): line_item = models.OneToOneField(LineItem, on_delete=models.CASCADE, related_name='costing') product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='costings', null=True, blank=True) # Denormalized? Already linked via line_item purchase_currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='purchase_costings') sale_currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='sale_costings') purchase_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) sale_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) # Denormalized? Already in line_item exchange_rate = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True) # Add other costing fields (freight, duty etc.) class Meta: db_table = 'costings' # CakePHP convention def __str__(self): return f"Costing for Line Item {self.line_item.id}" class Quote(models.Model): # Represents the specific details of a Quotation document document = models.OneToOneField(Document, on_delete=models.CASCADE, primary_key=True, related_name='quote') # Links to the base document enquiry = models.ForeignKey(Enquiry, on_delete=models.CASCADE, related_name='quotes') # Link back to the enquiry currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='quotes') title = models.CharField(max_length=255, help_text="e.g., Quotation Number, can be same as Enquiry title") revision = models.CharField(max_length=10, blank=True, null=True) delivery_time = models.CharField(max_length=100, blank=True, null=True, verbose_name="Delivery Time") payment_terms = models.CharField(max_length=100, blank=True, null=True, verbose_name="Payment Terms") days_valid = models.PositiveIntegerField(null=True, blank=True, verbose_name="Validity (Days)") issue_date = models.DateField(null=True, blank=True) class Meta: db_table = 'quotes' # CakePHP convention def __str__(self): return f"Quote: {self.title}" class Invoice(models.Model): document = models.OneToOneField(Document, on_delete=models.CASCADE, primary_key=True, related_name='invoice') enquiry = models.ForeignKey(Enquiry, on_delete=models.SET_NULL, null=True, blank=True, related_name='invoices') # Invoice might relate to multiple jobs/enquiries? job = models.ForeignKey(Job, on_delete=models.SET_NULL, null=True, blank=True, related_name='invoices') customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='invoices') currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='invoices') title = models.CharField(max_length=100, unique=True, help_text="Invoice Number (e.g., CMCIN...)") issue_date = models.DateField() due_date = models.DateField(null=True, blank=True) paid = models.BooleanField(default=False) date_paid = models.DateField(null=True, blank=True) class Meta: db_table = 'invoices' # CakePHP convention ordering = ['-issue_date'] def __str__(self): return f"Invoice: {self.title}" class PurchaseOrder(models.Model): document = models.OneToOneField(Document, on_delete=models.CASCADE, primary_key=True, related_name='purchaseorder') principle = models.ForeignKey(Principle, on_delete=models.PROTECT, related_name='purchase_orders') currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='purchase_orders') title = models.CharField(max_length=100, unique=True, help_text="PO Number (e.g., CMCPO...)") issue_date = models.DateField(null=True, blank=True) # HABTM relationship defined via the Job model jobs = models.ManyToManyField(Job, related_name='purchase_orders', blank=True, db_table='jobs_purchase_orders') # Explicit join table class Meta: db_table = 'purchase_orders' # CakePHP convention ordering = ['-issue_date', 'title'] def __str__(self): return f"PO: {self.title}" class OrderAcknowledgement(models.Model): document = models.OneToOneField(Document, on_delete=models.CASCADE, primary_key=True, related_name='orderacknowledgement') enquiry = models.ForeignKey(Enquiry, on_delete=models.SET_NULL, null=True, blank=True, related_name='order_acknowledgements') job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='order_acknowledgements') # Usually linked to one job currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='order_acknowledgements') title = models.CharField(max_length=255, help_text="e.g., Order Acknowledgement Reference") revision = models.CharField(max_length=10, blank=True, null=True) delivery_time = models.CharField(max_length=100, blank=True, null=True, verbose_name="Est. Delivery Time") payment_terms = models.CharField(max_length=100, blank=True, null=True, verbose_name="Payment Terms") issue_date = models.DateField(null=True, blank=True) class Meta: db_table = 'order_acknowledgements' # CakePHP convention def __str__(self): return f"Order Ack for Job: {self.job.title}" class PackingList(models.Model): document = models.OneToOneField(Document, on_delete=models.CASCADE, primary_key=True, related_name='packinglist') enquiry = models.ForeignKey(Enquiry, on_delete=models.SET_NULL, null=True, blank=True, related_name='packing_lists') job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='packing_lists') # Usually linked to one job customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='packing_lists') # Customer receiving currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='packing_lists') # Currency for value declaration? title = models.CharField(max_length=100, help_text="Packing List Reference") issue_date = models.DateField() class Meta: db_table = 'packing_lists' # CakePHP convention ordering = ['-issue_date'] def __str__(self): return f"Packing List for Job: {self.job.title}" # --- Email Related Models (from Vault script context) --- class Email(models.Model): # Link to the internal user who sent the email, if known and applicable user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, # Sender might be external or unknown blank=True, related_name='sent_emails', help_text="Internal sender, if applicable" ) subject = models.CharField(max_length=500) # Allow longer subjects udate = models.DateTimeField(db_index=True, verbose_name="Date Received/Sent") filename = models.CharField(max_length=255, blank=True, null=True, help_text="Original filename in vault") created_at = models.DateTimeField(auto_now_add=True) # HABTM relationships from CakePHP model enquiries = models.ManyToManyField(Enquiry, related_name='emails', blank=True, db_table='emails_enquiries') # Explicit join table name invoices = models.ManyToManyField(Invoice, related_name='emails', blank=True, db_table='emails_invoices') purchase_orders = models.ManyToManyField(PurchaseOrder, related_name='emails', blank=True, db_table='emails_purchase_orders') jobs = models.ManyToManyField(Job, related_name='emails', blank=True, db_table='emails_jobs') # email_attachment_count needs manual implementation class Meta: db_table = 'emails' # CakePHP convention ordering = ['-udate'] def __str__(self): sender = self.user.get_username() if self.user else 'Unknown Sender' # Use username for internal user # If sender is external, you might need to look up the 'From' EmailRecipient # For simplicity, this only shows internal sender or 'Unknown' return f"Email: {self.subject} (From: {sender})" class EmailRecipient(models.Model): email = models.ForeignKey(Email, on_delete=models.CASCADE, related_name='recipients') # Link to the Contact model, assuming the vault script creates Contact records for recipients contact = models.ForeignKey( Contact, on_delete=models.CASCADE, # If contact is deleted, remove recipient link? related_name='received_emails', help_text="Recipient Contact (created_at by vault script if unknown)" ) type = models.CharField(max_length=4, choices=EMAIL_RECIPIENT_TYPES) class Meta: db_table = 'email_recipients' # CakePHP convention # A contact can receive the same email multiple times if in To and Cc? # unique_together = ('email', 'contact', 'type') # Prevent duplicates per email/contact/type ordering = ['email', 'type'] def __str__(self): return f"{self.get_type_display()}: {self.contact.email} (Email ID: {self.email.id})" class EmailAttachment(models.Model): email = models.ForeignKey(Email, on_delete=models.CASCADE, related_name='email_attachments') # Changed related_name # Store relative path from MEDIA_ROOT, or use a dedicated storage backend # Name field stores the relative path + filename generated by vault script name = models.CharField(max_length=500, help_text="Path relative to attachment base dir") filename = models.CharField(max_length=255, help_text="Original filename from email") type = models.CharField(max_length=100) # MIME type size = models.PositiveIntegerField() is_message_body = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) # Consider adding a FileField if you want Django to manage the files directly # file = models.FileField(upload_to='email_attachments/', null=True, blank=True) class Meta: db_table = 'email_attachments' # CakePHP convention ordering = ['email', '-is_message_body', '-size'] # Show body first, then largest def __str__(self): return self.filename # --- Shipment Related Models --- class Shipment(models.Model): title = models.CharField(max_length=100, blank=True, null=True, help_text="Optional shipment title/reference") freight_forwarder = models.ForeignKey(FreightForwarder, on_delete=models.PROTECT, related_name='shipments') freight_service = models.ForeignKey(FreightService, on_delete=models.PROTECT, related_name='shipments') # Link to the internal user who created_at the shipment user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_at_shipments', help_text="Internal user who created_at this shipment" ) customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='shipments') # Receiving customer address = models.ForeignKey(Address, on_delete=models.PROTECT, related_name='shipments') # Delivery address airway_bill = models.CharField(max_length=100, blank=True, null=True, verbose_name="Air Waybill / Tracking") ship_date = models.DateField(null=True, blank=True) expected_delivery_date = models.DateField(null=True, blank=True) actual_delivery_date = models.DateField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # HABTM relationships jobs = models.ManyToManyField(Job, related_name='shipments', blank=True, db_table='shipments_jobs') principles = models.ManyToManyField(Principle, related_name='shipments', blank=True, db_table='shipments_principles') purchase_orders = models.ManyToManyField(PurchaseOrder, related_name='shipments', blank=True, db_table='shipments_purchase_orders') # Counter cache fields need manual implementation # box_count = models.PositiveIntegerField(default=0) # shipment_invoice_count = models.PositiveIntegerField(default=0) class Meta: db_table = 'shipments' # CakePHP convention ordering = ['-created_at'] def __str__(self): return f"Shipment {self.id} via {self.freight_forwarder.name} ({self.airway_bill or 'No Tracking'})" class Box(models.Model): shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE, related_name='boxes') box_number = models.PositiveIntegerField(null=True, blank=True) length = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True, help_text="in cm") width = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True, help_text="in cm") height = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True, help_text="in cm") weight = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True, help_text="in kg") class Meta: db_table = 'boxes' # CakePHP convention ordering = ['shipment', 'box_number'] verbose_name_plural = "Boxes" def __str__(self): return f"Box {self.box_number or self.id} for Shipment {self.shipment.id}" class ShipmentInvoice(models.Model): # Represents invoices related *to* a shipment (e.g., freight costs), not *in* a shipment shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE, related_name='related_invoices') principle = models.ForeignKey(Principle, on_delete=models.SET_NULL, null=True, blank=True, related_name='shipment_invoices') # Who issued invoice? freight_forwarder = models.ForeignKey(FreightForwarder, on_delete=models.SET_NULL, null=True, blank=True, related_name='invoices') # Or maybe FF issued it? currency = models.ForeignKey(Currency, on_delete=models.PROTECT, related_name='shipment_invoices') invoice_number = models.CharField(max_length=100) issue_date = models.DateField() amount = models.DecimalField(max_digits=12, decimal_places=2) description = models.CharField(max_length=255, blank=True, null=True) class Meta: db_table = 'shipment_invoices' # CakePHP convention ordering = ['shipment', '-issue_date'] def __str__(self): return f"Invoice {self.invoice_number} for Shipment {self.shipment.id}" # --- Authentication / Authorization Related --- # Group model is usually handled by django.contrib.auth.models.Group # If you need custom group logic, define it here. Otherwise, use Django's built-in. # Example: If you needed a custom Group profile linked to Django's Group # from django.contrib.auth.models import Group # class GroupProfile(models.Model): # group = models.OneToOneField(Group, on_delete=models.CASCADE, related_name='profile') # # Add custom fields for the group # description = models.TextField(blank=True) # # class Meta: # db_table = 'group_profiles' # Example # Example: Profile model to extend the built-in User # class Profile(models.Model): # user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) # # Add extra fields for internal users here, e.g.: # # phone_extension = models.CharField(max_length=10, blank=True) # # department = models.CharField(max_length=50, blank=True) # # class Meta: # db_table = 'profiles' # Example # # def __str__(self): # return f"{self.user.username}'s Profile"