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/views.ori
from datetime import date as _date
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.db import transaction
from django.forms import inlineformset_factory

from .forms import QuotationHeaderForm #QuotationLineForm, BaseLineFormSet
from .models import SalesQuotation, SalesQuotationLine, SalesNumberSequence,UOM,SalesService,PaymentTerm,Currency
from decimal import Decimal
from geo.models import Location   # <--- tambahin ini
from django.db.models import Q
from django.utils import timezone

from django.contrib import messages



STATUS_BADGES = {
    "draft": "secondary",
    "confirmed": "success",
    "cancelled": "danger",
    "expired": "warning",
}

ALLOWED_DELETE_STATUSES = {"draft", "cancelled"}

def freight_quotation_list(request):
    # ==== BULK ACTION (POST)
    if request.method == "POST":
        action = request.POST.get("action") or ""
        ids = request.POST.getlist("ids[]")
        if action == "delete" and ids:
            qs = SalesQuotation.objects.filter(id__in=ids)
            deletable = qs.filter(status__in=ALLOWED_DELETE_STATUSES)
            blocked   = qs.exclude(status__in=ALLOWED_DELETE_STATUSES)
            n_ok = deletable.count()
            n_block = blocked.count()
            # hapus yang boleh
            deletable.delete()
            if n_ok:
                messages.success(request, f"Deleted {n_ok} quotation(s) (draft/cancelled).")
            if n_block:
                messages.warning(request, f"Skipped {n_block} quotation(s) (not draft/cancelled).")
            return redirect("sales:freight_quotation_list")

    # ==== FILTERS (GET)
    q = (request.GET.get("q") or "").strip()
    status = (request.GET.get("status") or "").strip().lower()       # draft/confirmed/cancelled/expired
    service_id = request.GET.get("service") or ""
    currency_id = request.GET.get("currency") or ""
    payment_term_id = request.GET.get("pt") or ""
    valid_from = request.GET.get("vf") or ""
    valid_to   = request.GET.get("vt") or ""

    qs = (SalesQuotation.objects
          .select_related("customer", "sales_service", "currency", "payment_term")
          .order_by("-id"))

    if q:
        qs = qs.filter(Q(number__icontains=q) | Q(customer__name__icontains=q))

    today = timezone.localdate()
    if status == "draft":
        qs = qs.filter(status__iexact="draft")
    elif status == "confirmed":
        qs = qs.filter(status__iexact="confirmed")
    elif status == "cancelled":
        qs = qs.filter(status__iexact="cancelled")
    elif status == "expired":
        qs = qs.filter(valid_until__lt=today)

    if service_id:
        qs = qs.filter(sales_service_id=service_id)
    if currency_id:
        qs = qs.filter(currency_id=currency_id)
    if payment_term_id:
        qs = qs.filter(payment_term_id=payment_term_id)
    if valid_from:
        qs = qs.filter(valid_until__gte=valid_from)
    if valid_to:
        qs = qs.filter(valid_until__lte=valid_to)

    qs = qs[:200]

    services   = SalesService.objects.all().order_by("name")
    currencies = Currency.objects.all().order_by("code")
    pterms     = PaymentTerm.objects.all().order_by("name")

    ctx = {
        "quotations": qs,
        "q": q,
        "status": status,
        "service_id": service_id,
        "currency_id": currency_id,
        "payment_term_id": payment_term_id,
        "valid_from": valid_from,
        "valid_to": valid_to,
        "services": services,
        "currencies": currencies,
        "pterms": pterms,
        "STATUS_BADGES": STATUS_BADGES,
        "ALLOWED_DELETE_STATUSES": ALLOWED_DELETE_STATUSES,
    }
    
    return render(request, "freight/quotation_list.html", ctx)


from datetime import date as _date
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.db import transaction
from django.forms import inlineformset_factory

from .forms import QuotationHeaderForm #QuotationLineForm, BaseLineFormSet
from .models import SalesQuotation, SalesQuotationLine, SalesNumberSequence,UOM,SalesService,PaymentTerm,Currency
from decimal import Decimal
from geo.models import Location   # <--- tambahin ini
from django.db.models import Q
from django.utils import timezone

from django.contrib import messages

VER = "fq-2step-dyn-v1"

def ping(request):
    return HttpResponse(f"pong {VER}")

def debug_status(request):
    return HttpResponse("debug ok")

def _next_sequence_number():
    period = _date.today().strftime("%Y%m")
    with transaction.atomic():
        seq, _ = SalesNumberSequence.objects.select_for_update().get_or_create(
            business_type="freight", period=period, defaults={"prefix": "FQ", "padding": 5, "last_no": 0}
        )
        seq.last_no += 1
        seq.save(update_fields=["last_no"])
        return f"{seq.prefix}{seq.period}-{str(seq.last_no).zfill(seq.padding)}"

def _make_line_formset():
    return inlineformset_factory(
        parent_model=SalesQuotation,
        model=SalesQuotationLine,
        form=QuotationLineForm,
        formset=BaseLineFormSet,
        fk_name="sales_quotation",  # sesuai nama FK di model line
        extra=1,
        can_delete=True,
    )

def quotation_add_header(request):
    if request.method == "POST":
        form = QuotationHeaderForm(request.POST)
        if form.is_valid():
            with transaction.atomic():
                obj = form.save(commit=False)
                if not getattr(obj, "date", None):
                    obj.date = _date.today()
                if not getattr(obj, "number", None) or str(getattr(obj, "number")).strip() == "":
                    obj.number = _next_sequence_number()
                obj.total_amount = 0  # ← set 0 saat buat header
                obj.save()
            # simpan pk di session supaya bisa pakai URL tanpa pk
            request.session["current_quotation_pk"] = obj.pk
            return redirect("sales:freight_quotation_add_lines_session")
    else:
        form = QuotationHeaderForm()
    return render(request, "freight/quotation_step1.html", {"form": form})


def quotation_add_lines(request, pk: int):
    quotation = get_object_or_404(SalesQuotation, pk=pk)
    LineFormSet = _make_line_formset()
    if request.method == "POST":
        formset = LineFormSet(request.POST, instance=quotation)
        if formset.is_valid():
            with transaction.atomic():
                formset.save()
                quotation.recalc_totals() 
            messages.success(request, f"Quotation #{quotation.pk} berhasil dibuat.")
            return redirect("sales:freight_quotation_list")
    else:
        formset = LineFormSet(instance=quotation)
    return render(request, "freight/quotation_step2.html", {"formset": formset, "quotation": quotation})

def quotation_add_lines_session(request):
    pk = request.session.get("current_quotation_pk")
    if not pk:
        messages.warning(request, "Session quotation hilang. Mulai dari Step-1.")
        return redirect("sales:freight_quotation_add")
    return freight_quotation_add_lines(request, pk)

def _parse_dec_id(s: str) -> Decimal:
    """'1.234,56' -> Decimal('1234.56'); aman untuk empty."""
    if s is None: return Decimal("0")
    s = str(s).strip()
    if not s: return Decimal("0")
    # hilangkan ribuan, ganti koma jadi titik
    s = s.replace(".", "").replace(",", ".")
    try:
        return Decimal(s)
    except InvalidOperation:
        return Decimal("0")
    

def freight_quotation_add_lines(request, pk: int):
    quotation = get_object_or_404(SalesQuotation, pk=pk)
    # list pilihan untuk dropdown
    locations = Location.objects.all().order_by("name")
    uoms = UOM.objects.all().order_by("name")

    if request.method == "POST":
        origins = request.POST.getlist("origin[]")
        dests   = request.POST.getlist("destination[]")
        descs   = request.POST.getlist("description[]")
        uom_ids = request.POST.getlist("uom[]")
        qtys    = request.POST.getlist("qty[]")
        prices  = request.POST.getlist("price[]")

        lines_to_create = []
        total = Decimal("0.00")
        any_valid = False

        # hapus semua line lama, lalu buat ulang (sederhana & konsisten)
        with transaction.atomic():
            SalesQuotationLine.objects.filter(sales_quotation=quotation).delete()

            for i in range(max(len(origins), len(dests), len(descs), len(uom_ids), len(qtys), len(prices))):
                origin_id = (origins[i] if i < len(origins) else "") or ""
                dest_id   = (dests[i]   if i < len(dests)   else "") or ""
                uom_id    = (uom_ids[i] if i < len(uom_ids) else "") or ""
                description = (descs[i] if i < len(descs) else "").strip()
                qty  = _parse_dec_id(qtys[i] if i < len(qtys) else "0")
                price= _parse_dec_id(prices[i] if i < len(prices) else "0")

                # lewati baris kosong total
                if not (origin_id or dest_id or description or qty or price or uom_id):
                    continue

                # minimal isi: origin, destination, uom, qty>0
                if not origin_id or not dest_id or not uom_id or qty <= 0:
                    # bisa ditambahkan kumpulan error per-baris jika perlu
                    continue

                try:
                    origin = Location.objects.get(pk=origin_id)
                    dest   = Location.objects.get(pk=dest_id)
                    uom    = UOM.objects.get(pk=uom_id)
                except (Location.DoesNotExist, Uom.DoesNotExist):
                    continue

                line_total = qty * price
                total += line_total
                any_valid = True
                lines_to_create.append(SalesQuotationLine(
                    sales_quotation=quotation,
                    origin=origin,
                    destination=dest,
                    description=description,
                    qty=qty,
                    uom=uom,
                    price=price,
                ))

            if not any_valid:
                messages.error(request, "Minimal satu line valid harus diisi.")
            else:
                SalesQuotationLine.objects.bulk_create(lines_to_create)
                quotation.total_amount = total
                quotation.save(update_fields=["total_amount"])
                messages.success(request, f"Quotation #{quotation.number} tersimpan. Grand total: {total:.2f}")
                return redirect("sales:freight_quotation_list")

    # GET atau POST invalid → render ulang
    # siapkan data baris awal: kalau belum ada, tampilkan 1 baris kosong
    existing = list(SalesQuotationLine.objects.filter(sales_quotation=quotation))

    return render(request, "freight/quotation_step2.html", {
        "quotation": quotation,
        "locations": locations,
        "uoms": uoms,
        "lines": existing,
    })