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/sales/job_order_model.py
from django.db import models
from django.db.models import PROTECT
from django.conf import settings
from django.db.models import Q

from core.models.services import Service
from core.models.currencies import Currency
from core.models.payment_terms import  PaymentTerm
from core.models.taxes import  Tax


from core.utils.numbering import get_next_number
from partners.models import Partner
from partners.models import Customer
from decimal import Decimal


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

    class Meta:
        abstract = True


class JobOrder(TimeStampedModel):
    """
    Jobfile untuk team sales.

    Satu baris = satu job penjualan, basisnya customer + service.
    """
    
    number = models.CharField("Job No", max_length=30, unique=True)
    order_number = models.CharField(
        max_length=30, 
        null=True,
        blank=True,
        help_text="Customer reference eg. PO#",
        
    )

    job_date = models.DateField("Date")

    service = models.ForeignKey(
        Service,
        on_delete=PROTECT,
        related_name="job_services",
        verbose_name="Service",
    )

    customer = models.ForeignKey(
        Customer,
        on_delete=PROTECT,
        related_name="customer_job",
        verbose_name="Customer",
        
    )

    cargo_description = models.CharField(
        max_length=255,
        blank=True,
        help_text="Deskripsi singkat cargo.",
    )

    quantity = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Jumlah / volume cargo (opsional).",
    )

    pickup = models.CharField(
        "Pick Up",
        max_length=255,
        blank=True,
        help_text="Alamat / lokasi penjemputan.",
    )

    delivery = models.CharField(
        "Delivery",
        max_length=255,
        blank=True,
        help_text="Alamat / lokasi pengantaran.",
    )

    pic = models.CharField(
        "PIC",
        max_length=100,
        blank=True,
        help_text="Nama PIC.",
    )

    payment_term = models.ForeignKey(
        PaymentTerm,
        on_delete=PROTECT,
        related_name="job_payments",
        verbose_name="Payment Term",
    )

    currency = models.ForeignKey(
        Currency,
        on_delete=PROTECT,
        default=1,  # sesuaikan default ID om
    )


    total_amount = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        default=0,
    )


    is_tax = models.BooleanField(default=False, verbose_name="Apply Tax 1.1 (VAT)")
    is_pph = models.BooleanField(default=False, verbose_name="Apply PPH")

    tax_amount = models.DecimalField(
        max_digits=12,
        decimal_places=2,
        default=0,
    )


    kurs_idr = models.DecimalField(
        max_digits=12,
        decimal_places=2,
        default=1,
    )

    total_in_idr = models.DecimalField(
        max_digits=12,
        decimal_places=2,
        default=0,
    )


    pph_amount = models.DecimalField(
        max_digits=12,
        decimal_places=2,
        default=0,
    )

    grand_total = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        default=0,
    )


    sales_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=PROTECT,
        related_name="job_sales",
        verbose_name="Sales",
        null=True,
        blank=True,
        
    )


    remarks_internal = models.TextField(
        blank=True,
        help_text="Catatan internal untuk tim.",
    )


    is_invoiced = models.BooleanField(
        default=False,
        help_text="Sudah dibuat invoice"
    )

    ST_IN_PROGRESS = "IN_PROGRESS"
    ST_PENDING = "PENDING"
    ST_CANCELLED = "CANCELLED"
    ST_COMPLETED = "COMPLETED"

    STATUS_CHOICES = [
        (ST_IN_PROGRESS, "In Progress"),
        (ST_PENDING, "Pending"),
        (ST_CANCELLED, "Cancelled"),
        (ST_COMPLETED, "Completed"),
    ]

    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default=ST_IN_PROGRESS,
        db_index=True,
    )

    class Meta:
        db_table = "sales_job_orders"
        ordering = ["-job_date", "-id"]
        verbose_name = "Job"
        verbose_name_plural = "Jobs"

    def __str__(self):
        return self.number

    def save(self, *args, **kwargs):
        if not self.number:
            # Pakai NumberSequence: app='sales', code='JOBFILE'
            self.number = get_next_number("sales", "JOB_ORDER")
        super().save(*args, **kwargs)

    
from decimal import Decimal
from django.db import models
from django.db.models import PROTECT

from partners.models import Vendor  # sesuaikan path


class JobCost(models.Model):
    class Category(models.TextChoices):
        VENDOR = "VENDOR", "Vendor Cost"
        MOBILIZATION = "MOBILIZATION", "Mobilization"
        LABOUR = "LABOUR", "Labour"
        DOCUMENT = "DOCUMENT", "Document"
        MISC = "MISC", "Misc"

    job_order = models.ForeignKey(
        "sales.JobOrder",
        on_delete=models.CASCADE,
        related_name="costs",
        db_index=True
    )

    # ✅ tambahan
    category = models.CharField(
        max_length=20,
        choices=Category.choices,
        default=Category.VENDOR,
        db_index=True,
    )

    # ✅ tambahan (boleh kosong)
    vendor = models.ForeignKey(
        Vendor,
        on_delete=PROTECT,
        null=True,
        blank=True,
        related_name="+",
        db_index=True,
    )

    # ✅ optional: keterangan kalau vendor kosong
    internal_note = models.CharField(max_length=120, blank=True)

    # existing
    description = models.CharField(max_length=255)
    qty = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    price = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    amount = models.DecimalField(max_digits=14, decimal_places=2, default=0)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "sales_job_costs"
        ordering = ["id"]

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

    def save(self, *args, **kwargs):
        self.amount = (self.qty or Decimal("0")) * (self.price or Decimal("0"))
        super().save(*args, **kwargs)

    @property
    def vendor_label(self):
        return self.vendor.name if self.vendor else (self.internal_note or "No Vendor / Internal")


class JobOrderAttachment(TimeStampedModel):
    job_order = models.ForeignKey(
        "JobOrder",
        on_delete=models.CASCADE,
        related_name="attachments",
        verbose_name="Job Order",
    )
    file = models.FileField(
        upload_to="job_orders/%Y/%m/",
        verbose_name="File",
    )
    description = models.CharField(
        max_length=255,
        blank=True,
        verbose_name="Description",
        help_text="Keterangan singkat file, misal: PO Customer, Kontrak, dll."
    )
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="joborder_attachments",
    )

    class Meta:
        verbose_name = "Job Order Attachment"
        verbose_name_plural = "Job Order Attachments"
        ordering = ["-created_at"]

    def __str__(self):
        return f"{self.job_order.number} - {self.filename}"

    @property
    def filename(self):
        return self.file.name.split("/")[-1]