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/projects/models.py
from django.db import models
from django.utils import timezone
from core.utils.numbering import get_next_number
from core.models.currencies import Currency

from django.db.models import PROTECT, CASCADE, F, Sum


class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class ProjectCategory(TimeStampedModel):
    code = models.CharField(max_length=30, unique=True)
    name = models.CharField(max_length=100)

    class Meta:
        db_table = "projects_categories"
        ordering = ["name"]

    def __str__(self):
        return self.name


class Project(TimeStampedModel):
    STATUS_DRAFT     = "DRAFT"
    STATUS_CONFIRMED = "CONFIRMED"
    STATUS_PROGRESS  = "ON_PROGRESS"
    STATUS_COMPLETED = "COMPLETED"
    STATUS_CANCELLED = "CANCELLED"
    STATUS_HOLD      = "ON_HOLD"

    STATUS_CHOICES = [
        (STATUS_DRAFT, "Draft"),
        (STATUS_CONFIRMED, "Confirmed"),
        (STATUS_PROGRESS, "On Progress"),
        (STATUS_COMPLETED, "Completed"),
        (STATUS_CANCELLED, "Cancelled"),
        (STATUS_HOLD, "On Hold"),
    ]

    # --- allowed transitions (disamakan dengan Sales) ---
    _ALLOWED_TRANSITIONS = {
        STATUS_DRAFT:     {STATUS_CONFIRMED, STATUS_CANCELLED},
        STATUS_CONFIRMED: {STATUS_PROGRESS, STATUS_CANCELLED, STATUS_HOLD},
        STATUS_PROGRESS:  {STATUS_COMPLETED, STATUS_CANCELLED, STATUS_HOLD},
        STATUS_HOLD:      {STATUS_PROGRESS, STATUS_CANCELLED},
        STATUS_COMPLETED: set(),
        STATUS_CANCELLED: set(),
    }

    number = models.CharField(max_length=50, unique=True, null=True, blank=True, editable=False)
    ref_number = models.CharField(max_length=50, null=True, blank=True)
    name = models.CharField(max_length=200)
    category = models.ForeignKey(ProjectCategory, on_delete=models.PROTECT, related_name="projects")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_DRAFT)
    start_date = models.DateField(null=True, blank=True)
    end_date = models.DateField(null=True, blank=True)
    description = models.TextField(blank=True)

    value_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0)
    value_currency_code = models.CharField(max_length=3, default="IDR")
    
    class Meta:
        db_table = "projects"
        indexes = [
            models.Index(fields=["status"]),
            models.Index(fields=["start_date"]),
            models.Index(fields=["number"]),
        ]
        ordering = ["-created_at"]

    def __str__(self):
        return f"{self.number or '—'} {self.name}"

    def save(self, *args, **kwargs):
        if not self.number:
            self.number = get_next_number(app_label="projects", code="PROJECT", today=timezone.localdate())
        super().save(*args, **kwargs)


class CostCategory(TimeStampedModel):
    code = models.CharField(max_length=30, unique=True)
    name = models.CharField(max_length=100)

    class Meta:
        db_table = "projects_cost_categories"
        ordering = ["name"]

    def __str__(self):
        return self.name


class ProjectCost(TimeStampedModel):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="costs")
    category = models.ForeignKey(CostCategory, on_delete=models.PROTECT, related_name="costs")
    title = models.CharField(max_length=200)
    amount = models.DecimalField(max_digits=14, decimal_places=2)
    currency_code = models.CharField(max_length=3, default="IDR")
    cost_date = models.DateField(null=True, blank=True)
    notes = models.TextField(blank=True)
    ref = models.CharField(max_length=100, null=True, blank=True)
    attachment = models.FileField(upload_to="projects/costs/%Y/%m/", null=True, blank=True)


    class Meta:
        db_table = "projects_costs"
        indexes = [
            models.Index(fields=["project", "category"]),
            models.Index(fields=["cost_date"]),
        ]
        ordering = ["-cost_date", "-created_at"]

    def __str__(self):
        return f"{self.title} ({self.amount} {self.currency_code})"


class ProjectStatus:
    DRAFT       = Project.STATUS_DRAFT
    CONFIRMED   = Project.STATUS_CONFIRMED
    ON_PROGRESS = Project.STATUS_PROGRESS
    COMPLETED   = Project.STATUS_COMPLETED
    CANCELLED   = Project.STATUS_CANCELLED
    ON_HOLD     = Project.STATUS_HOLD

    choices = Project.STATUS_CHOICES