Files
dashboard-cpsp-executive/backend/apps/farms/models.py
T
2026-09-08 15:21:23 +07:00

277 lines
9.2 KiB
Python

from datetime import date
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from apps.accounts.models import User
class Site(models.Model):
site_id = models.BigAutoField(primary_key=True)
site_name = models.CharField(max_length=30)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites")
# Linked when GM/GM Admin adds the city dashboard to their Pengaturan view list.
city_site = models.OneToOneField(
"sync.CitySite",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="hq_site",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "sites"
ordering = ["site_name"]
def __str__(self) -> str:
return self.site_name
class Kandang(models.Model):
kandang_id = models.BigAutoField(primary_key=True)
kandang_name = models.CharField(max_length=30)
feed_in_button_urls = models.JSONField(default=list, blank=True)
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="kandangs")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "kandang"
ordering = ["kandang_name"]
def __str__(self) -> str:
return self.kandang_name
class Flock(models.Model):
flock_id = models.BigAutoField(primary_key=True)
flock_name = models.CharField(max_length=30)
kandang = models.ForeignKey(
Kandang,
on_delete=models.CASCADE,
related_name="flocks",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "flock"
ordering = ["flock_name"]
def __str__(self) -> str:
return self.flock_name
class Cycle(models.Model):
STATUS_ACTIVE = "active"
STATUS_PENDING_CLOSE = "pending_close"
STATUS_CLOSED = "closed"
STATUS_CHOICES = [
(STATUS_ACTIVE, "Active"),
(STATUS_PENDING_CLOSE, "Pending close"),
(STATUS_CLOSED, "Closed"),
]
cycle_id = models.BigAutoField(primary_key=True)
total_days = models.IntegerField(default=1)
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
doc_in_weight = models.IntegerField()
doc_in_count = models.IntegerField()
feed_initial_balance = models.IntegerField(default=0)
feed_initial_balance_date = models.DateField(null=True, blank=True)
status = models.CharField(
max_length=30, choices=STATUS_CHOICES, default=STATUS_ACTIVE, db_index=True
)
kandang = models.ForeignKey(Kandang, on_delete=models.CASCADE, related_name="cycles")
city_site = models.ForeignKey(
"sync.CitySite",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="mirrored_cycles",
)
source_cycle_id = models.BigIntegerField(
null=True,
blank=True,
help_text="cycle_id on the city dashboard-cpsp deployment",
)
proposed_end_date = models.DateField(null=True, blank=True)
close_requested_at = models.DateTimeField(null=True, blank=True)
close_requested_by = models.ForeignKey(
User,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="requested_cycle_closes",
)
close_approved_by = models.ForeignKey(
User,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="approved_cycle_closes",
)
close_approved_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "cycles"
ordering = ["-start_date"]
constraints = [
models.CheckConstraint(
check=models.Q(end_date__isnull=True)
| models.Q(end_date__gte=models.F("start_date")),
name="chk_cycles_date_range",
),
models.UniqueConstraint(
fields=["city_site", "source_cycle_id"],
condition=models.Q(city_site__isnull=False, source_cycle_id__isnull=False),
name="uniq_cycle_city_source",
),
]
def __str__(self) -> str:
return f"Cycle #{self.pk} ({self.status})"
def clean(self) -> None:
if self.end_date and self.start_date and self.end_date < self.start_date:
raise ValidationError({"end_date": "Must be on or after start_date"})
if (
self.proposed_end_date
and self.start_date
and self.proposed_end_date < self.start_date
):
raise ValidationError(
{"proposed_end_date": "Must be on or after start_date"}
)
def soft_end_date(self) -> date:
if self.end_date:
return self.end_date
from apps.operations.services.visibility import visible_through_date
return visible_through_date()
def resolved_status(self) -> str:
return self.status
def sync_status(self) -> bool:
return False
def save(self, *args, **kwargs):
self.sync_status()
if kwargs.get("update_fields") is None:
if self.end_date and self.start_date:
self.total_days = max((self.end_date - self.start_date).days + 1, 1)
elif self.start_date and not self.end_date:
soft = self.soft_end_date()
self.total_days = max((soft - self.start_date).days + 1, 1)
super().save(*args, **kwargs)
def clear_close_request_fields(self) -> None:
self.proposed_end_date = None
self.close_requested_at = None
self.close_requested_by = None
def mark_pending_close(self, proposed_end_date: date, requested_at=None) -> None:
if proposed_end_date < self.start_date:
raise ValidationError(
{"proposed_end_date": "Must be on or after start_date"}
)
self.status = self.STATUS_PENDING_CLOSE
self.proposed_end_date = proposed_end_date
self.close_requested_at = requested_at or timezone.now()
self.close_approved_by = None
self.close_approved_at = None
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_approved_by",
"close_approved_at",
"updated_at",
]
)
def cancel_pending_close(self) -> None:
if self.status == self.STATUS_ACTIVE and self.proposed_end_date is None:
return
if self.status == self.STATUS_CLOSED:
raise ValidationError("Cycle is already closed.")
self.status = self.STATUS_ACTIVE
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
def approve_close(self, approver: User) -> None:
if self.status != self.STATUS_PENDING_CLOSE:
raise ValidationError("Cycle is not pending close approval.")
if not self.proposed_end_date:
raise ValidationError("proposed_end_date is missing.")
self.status = self.STATUS_CLOSED
self.end_date = self.proposed_end_date
self.total_days = max((self.end_date - self.start_date).days + 1, 1)
self.close_approved_by = approver
self.close_approved_at = timezone.now()
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"end_date",
"total_days",
"close_approved_by",
"close_approved_at",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
def reject_close(self, _actor: User) -> None:
if self.status != self.STATUS_PENDING_CLOSE:
raise ValidationError("Cycle is not pending close approval.")
self.status = self.STATUS_ACTIVE
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
@property
def current_day(self) -> int:
from apps.operations.services.visibility import visible_through_date
effective = visible_through_date()
if effective < self.start_date:
return 0
elapsed = (effective - self.start_date).days
if self.end_date:
max_day = max(self.total_days - 1, 0)
return min(max(elapsed, 0), max_day)
return max(elapsed, 0)
@property
def visible_through_date(self) -> date:
from apps.operations.services.visibility import visible_through_date as cutoff_date
return cutoff_date()