HEX
Server: nginx/1.18.0
System: Linux mail.dakarash.co.id 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User: www-data (33)
PHP: 8.1.2-1ubuntu2.23
Disabled: NONE
Upload Files
File: /home/django/apps/cargochains/timestamps.patch
*** a/sales/models.py
--- b/sales/models.py
***************
-# sales/models.py
-from django.db import models
-from django.db.models import PROTECT, CASCADE
-from partners.models import Partner
-from geo.models import Location
-from decimal import Decimal
+# sales/models.py
+from django.db import models
+from django.db.models import PROTECT, CASCADE, F, Sum
+from django.utils import timezone
+from partners.models import Partner
+from geo.models import Location
+from decimal import Decimal
+
+class TimestampedModel(models.Model):
+    """
+    Timestamp base model:
+    - created_at & updated_at NOT NULL, default timezone.now (aman untuk loaddata/fixtures)
+    - updated_at auto-update on save()
+    """
+    created_at = models.DateTimeField(default=timezone.now, editable=False, db_index=True)
+    updated_at = models.DateTimeField(default=timezone.now)
+
+    class Meta:
+        abstract = True
+
+    def save(self, *args, **kwargs):
+        if self.pk:
+            self.updated_at = timezone.now()
+        super().save(*args, **kwargs)
***************
-class Currency(models.Model):
+class Currency(TimestampedModel):
     name = models.CharField(max_length=100)
     code = models.CharField(max_length=10, unique=True)  # contoh: IDR, USD
     symbol = models.CharField(max_length=8, blank=True, null=True)
     decimals = models.PositiveSmallIntegerField(default=2)
     is_active = models.BooleanField(default=True)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "currencies"
         ordering = ["code"]
***************
-class SalesService(models.Model):
+class SalesService(TimestampedModel):
     code = models.CharField(max_length=30, unique=True)
     name = models.CharField(max_length=100)
     sort_order = models.IntegerField(default=0)
     is_active = models.BooleanField(default=True)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "sales_services"
         ordering = ["sort_order", "name"]
***************
-class PaymentTerm(models.Model):
+class PaymentTerm(TimestampedModel):
     code = models.CharField(max_length=30, unique=True)
     name = models.CharField(max_length=100)
     days = models.PositiveSmallIntegerField(default=0)
     description = models.TextField(blank=True, null=True)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "payment_terms"
         ordering = ["name"]
***************
-class UOM(models.Model):
+class UOM(TimestampedModel):
     code = models.CharField(max_length=30, unique=True)
     name = models.CharField(max_length=100)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "uoms"
         ordering = ["name"]
***************
-class SalesNumberSequence(models.Model):
+class SalesNumberSequence(TimestampedModel):
     business_type = models.CharField(max_length=20, default="freight")
     period = models.CharField(max_length=6)  # YYYYMM
     prefix = models.CharField(max_length=30, default="FQ")
     padding = models.PositiveSmallIntegerField(default=5)
     last_no = models.PositiveIntegerField(default=0)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "sales_number_sequences"
         unique_together = (("business_type", "period"),)
         ordering = ["-period"]
***************
-class SalesQuotation(models.Model):
+class SalesQuotation(TimestampedModel):
     STATUS_DRAFT     = "DRAFT"
     STATUS_SENT      = "SENT"
     STATUS_ACCEPTED  = "ACCEPTED"
     STATUS_CANCELLED = "CANCELLED"
     STATUS_EXPIRED   = "EXPIRED"
@@
-    # contoh field yang sudah ada:
-    # number = models.CharField(max_length=50, unique=True)
-    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=STATUS_DRAFT)
-    # valid_until = models.DateField(null=True, blank=True)  # pastikan ada field ini
+    # status utama
+    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=STATUS_DRAFT)
 
     # Transisi yang diizinkan:
     # DRAFT -> SENT
     # SENT -> {ACCEPTED, CANCELLED, EXPIRED}
     # ACCEPTED -> {CANCELLED}  (opsional; hapus jika tak diinginkan)
@@
     def status_badge_class(self) -> str:
         return {
             self.STATUS_DRAFT: "bg-secondary",
             self.STATUS_SENT: "bg-info",
             self.STATUS_ACCEPTED: "bg-success",
             self.STATUS_CANCELLED: "bg-danger",
             self.STATUS_EXPIRED: "bg-warning",
         }.get(self.status, "bg-secondary")
 
     number = models.CharField(max_length=50, unique=True)
     customer = models.ForeignKey(Partner, on_delete=PROTECT, related_name="quotations")
     date = models.DateField(null=True, blank=True)
     valid_until = models.DateField()
     total_amount = models.DecimalField(max_digits=18, decimal_places=2, default=Decimal("0.00"))
@@
     currency = models.ForeignKey(Currency, on_delete=PROTECT, related_name="quotations")
 
     payment_term = models.ForeignKey(
         PaymentTerm, on_delete=PROTECT, related_name="quotations", null=True, blank=True
     )
@@
-    amount_total = models.DecimalField(max_digits=18, decimal_places=2, default=0)
-    status = models.CharField(max_length=20, default="DRAFT")
+    # HAPUS DUPLIKAT: amount_total & status duplikat
     business_type = models.CharField(max_length=20, default="freight")
@@
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
-
     class Meta:
         db_table = "sales_quotations"
@@
     def recalc_totals(self):
         """
         Hitung Σ(qty * price) dari semua line aktif dan simpan ke total_amount.
         """
-        total = (
-            self.lines.annotate(line_total=F("qty") * F("price"))
-            .aggregate(s=Sum("line_total"))["s"]
-            or Decimal("0.00")
-        )
+        total = (self.lines
+                 .annotate(line_total=F("qty") * F("price"))
+                 .aggregate(s=Sum("line_total"))["s"] or Decimal("0.00"))
         self.total_amount = total
         self.save(update_fields=["total_amount"])
         return total
***************
-class SalesQuotationLine(models.Model):
-    sales_quotation = models.ForeignKey(SalesQuotation, on_delete=CASCADE, related_name="lines")
+class SalesQuotationLine(TimestampedModel):
+    sales_quotation = models.ForeignKey(SalesQuotation, on_delete=CASCADE, related_name="lines")
     origin = models.ForeignKey(Location, on_delete=PROTECT, related_name="quotation_origin_lines")
     destination = models.ForeignKey(Location, on_delete=PROTECT, related_name="quotation_destination_lines")
     description = models.CharField(max_length=200, blank=True, null=True)
     uom = models.ForeignKey(UOM, on_delete=PROTECT)
     qty = models.DecimalField(max_digits=18, decimal_places=3, default=0)
     price = models.DecimalField(max_digits=18, decimal_places=2, default=0)
     amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "sales_quotation_lines"
@@
-class SalesOrder(models.Model):
+class SalesOrder(TimestampedModel):
     number = models.CharField(max_length=50, unique=True)
     quotation = models.ForeignKey(
         SalesQuotation, on_delete=models.SET_NULL, null=True, blank=True, related_name="orders"
     )
     customer = models.ForeignKey(Partner, on_delete=PROTECT, related_name="orders")
@@
     status = models.CharField(max_length=20, default="DRAFT")
     business_type = models.CharField(max_length=20, default="freight")
@@
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
-
     class Meta:
         db_table = "sales_orders"
***************
-class SalesOrderLine(models.Model):
+class SalesOrderLine(TimestampedModel):
     sales_order = models.ForeignKey(SalesOrder, on_delete=CASCADE, related_name="lines")
     origin = models.ForeignKey(Location, on_delete=PROTECT, related_name="order_origin_lines")
     destination = models.ForeignKey(Location, on_delete=PROTECT, related_name="order_destination_lines")
     description = models.CharField(max_length=200, blank=True, null=True)
     uom = models.ForeignKey(UOM, on_delete=PROTECT)
     qty = models.DecimalField(max_digits=18, decimal_places=3, default=0)
     price = models.DecimalField(max_digits=18, decimal_places=2, default=0)
     amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
-
-    # timestamps (PALING BELAKANG)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         db_table = "sales_order_lines"