Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+337
View File
@@ -0,0 +1,337 @@
from __future__ import annotations
import re
from datetime import date, datetime, timezone
from typing import Iterable
from sqlalchemy import func, select
from sqlalchemy.orm import Session, selectinload
from app.modules.services.models import (
ClientServiceSubscription,
ServiceCatalogue,
ServiceDueDateExtension,
ServiceDueDateRule,
)
DUE_PERIOD_TYPES = [
("yearly", "Yearly / Annual"),
("monthly", "Monthly"),
("quarterly", "Quarterly"),
("one_time", "One Time"),
("event_based", "Event Based"),
("renewal_based", "Renewal Before Expiry"),
("custom", "Custom / Manual"),
]
DUE_YEAR_BASIS_CHOICES = [
("assessment_year_start", "Assessment year start year"),
("financial_year_start", "Financial year start year"),
("financial_year_end", "Financial year end year"),
("calendar_year", "Calendar year from period"),
]
DUE_DATE_SOURCE_RULE = "rule"
DUE_DATE_SOURCE_EXTENSION = "extension"
DUE_DATE_SOURCE_MANUAL = "manual"
def parse_optional_date(value: str | None) -> date | None:
value = (value or "").strip()
if not value:
return None
return date.fromisoformat(value)
def _parse_year_pair(value: str | None) -> tuple[int, int] | None:
value = (value or "").strip()
match = re.match(r"^(\d{4})\s*-\s*(\d{2}|\d{4})$", value)
if not match:
return None
start = int(match.group(1))
end_raw = match.group(2)
end = int(end_raw) if len(end_raw) == 4 else int(str(start)[:2] + end_raw)
return start, end
def _month_add(year: int, month: int, offset: int) -> tuple[int, int]:
index = (year * 12 + (month - 1)) + int(offset or 0)
return index // 12, index % 12 + 1
def _safe_date(year: int, month: int, day: int) -> date | None:
try:
return date(int(year), int(month), int(day))
except Exception:
return None
def _period_month_from_label(financial_year: str | None, period_label: str | None) -> tuple[int, int] | None:
label = (period_label or "").strip().lower()
fy = _parse_year_pair(financial_year)
iso_match = re.match(r"^(\d{4})[-/](\d{1,2})$", label)
if iso_match:
return int(iso_match.group(1)), int(iso_match.group(2))
month_names = {
"apr": 4, "april": 4, "may": 5, "jun": 6, "june": 6, "jul": 7, "july": 7,
"aug": 8, "august": 8, "sep": 9, "sept": 9, "september": 9, "oct": 10, "october": 10,
"nov": 11, "november": 11, "dec": 12, "december": 12, "jan": 1, "january": 1,
"feb": 2, "february": 2, "mar": 3, "march": 3,
}
if label in month_names and fy:
month = month_names[label]
year = fy[0] if month >= 4 else fy[1]
return year, month
return None
def _quarter_end_from_label(financial_year: str | None, period_label: str | None) -> tuple[int, int] | None:
label = (period_label or "").strip().lower().replace(" ", "")
fy = _parse_year_pair(financial_year)
if not fy:
return None
mapping = {
"q1": (fy[0], 6), "quarter1": (fy[0], 6), "apr-jun": (fy[0], 6),
"q2": (fy[0], 9), "quarter2": (fy[0], 9), "jul-sep": (fy[0], 9),
"q3": (fy[0], 12), "quarter3": (fy[0], 12), "oct-dec": (fy[0], 12),
"q4": (fy[1], 3), "quarter4": (fy[1], 3), "jan-mar": (fy[1], 3),
}
return mapping.get(label)
def calculate_due_date(
rule: ServiceDueDateRule | None,
*,
financial_year: str | None,
assessment_year: str | None = None,
period_label: str | None = None,
expiry_date: date | None = None,
) -> date | None:
"""Calculate a statutory due date from a catalogue due-date rule.
The function is intentionally conservative. If the rule needs a period label
and the engagement does not yet carry one, it returns None instead of
guessing. This preserves existing engagement creation behaviour.
"""
if not rule or not getattr(rule, "is_active", True):
return None
day = getattr(rule, "due_day", None)
period_type = (getattr(rule, "period_type", None) or "yearly").strip().lower()
due_month = getattr(rule, "due_month", None)
month_offset = int(getattr(rule, "due_month_offset", None) or 0)
if period_type in {"renewal_based", "before_expiry", "expiry_based"}:
if not expiry_date:
return None
days_before = int(getattr(rule, "renewal_days_before_expiry", None) or 0)
from datetime import timedelta
return expiry_date - timedelta(days=days_before)
if not day:
return None
if period_type in {"yearly", "one_time"}:
if not due_month:
return None
basis = (getattr(rule, "due_year_basis", None) or "assessment_year_start").strip().lower()
fy = _parse_year_pair(financial_year)
ay = _parse_year_pair(assessment_year)
if basis == "financial_year_start" and fy:
year = fy[0]
elif basis == "financial_year_end" and fy:
year = fy[1]
elif ay:
year = ay[0]
elif fy:
year = fy[1]
else:
return None
return _safe_date(year, int(due_month), int(day))
if period_type == "monthly":
period = _period_month_from_label(financial_year, period_label)
if not period:
return None
year, month = _month_add(period[0], period[1], month_offset)
return _safe_date(year, month, int(day))
if period_type == "quarterly":
period = _quarter_end_from_label(financial_year, period_label)
if not period:
return None
year, month = _month_add(period[0], period[1], month_offset)
return _safe_date(year, month, int(day))
return None
def get_active_due_rule_for_catalogue(db: Session, *, catalogue_id: int) -> ServiceDueDateRule | None:
return db.execute(
select(ServiceDueDateRule)
.where(
ServiceDueDateRule.service_catalogue_id == catalogue_id,
ServiceDueDateRule.is_active.is_(True),
)
.order_by(ServiceDueDateRule.sort_order.asc(), ServiceDueDateRule.id.asc())
).scalars().first()
def list_due_rules(db: Session, *, catalogue_id: int, include_inactive: bool = True) -> list[ServiceDueDateRule]:
query = select(ServiceDueDateRule).where(ServiceDueDateRule.service_catalogue_id == catalogue_id)
if not include_inactive:
query = query.where(ServiceDueDateRule.is_active.is_(True))
return db.execute(query.order_by(ServiceDueDateRule.sort_order.asc(), ServiceDueDateRule.id.asc())).scalars().all()
def get_due_rule(db: Session, *, rule_id: int, catalogue_id: int | None = None) -> ServiceDueDateRule | None:
query = select(ServiceDueDateRule).where(ServiceDueDateRule.id == rule_id)
if catalogue_id:
query = query.where(ServiceDueDateRule.service_catalogue_id == catalogue_id)
return db.execute(query).scalar_one_or_none()
def list_due_extensions(db: Session, *, catalogue_id: int, tenant_id: int | None = None, limit: int = 20) -> list[ServiceDueDateExtension]:
query = (
select(ServiceDueDateExtension)
.options(selectinload(ServiceDueDateExtension.due_rule))
.where(ServiceDueDateExtension.service_catalogue_id == catalogue_id)
)
if tenant_id:
query = query.where(ServiceDueDateExtension.tenant_id == tenant_id)
return db.execute(
query.order_by(ServiceDueDateExtension.id.desc()).limit(limit)
).scalars().all()
def apply_due_date_rule_to_subscription(
db: Session,
subscription: ClientServiceSubscription,
*,
force: bool = False,
) -> date | None:
if getattr(subscription, "is_locked", False):
return getattr(subscription, "current_due_date", None)
rule = get_active_due_rule_for_catalogue(db, catalogue_id=subscription.service_catalogue_id)
rule_type = (getattr(rule, "period_type", None) or "").strip().lower() if rule else ""
# For normal statutory rules, preserve an already calculated/extended due date.
# For renewal-based rules, recalculate when expiry_date changes, unless the due date was manually overridden/extended.
if getattr(subscription, "current_due_date", None) and not force:
if rule_type not in {"renewal_based", "before_expiry", "expiry_based"}:
return subscription.current_due_date
if getattr(subscription, "due_date_source", None) in {DUE_DATE_SOURCE_EXTENSION, DUE_DATE_SOURCE_MANUAL}:
return subscription.current_due_date
calculated = calculate_due_date(
rule,
financial_year=subscription.financial_year,
assessment_year=subscription.assessment_year,
period_label=getattr(subscription, "period_label", None),
expiry_date=getattr(subscription, "expiry_date", None),
)
if calculated:
subscription.due_date_rule_id = rule.id if rule else None
subscription.original_due_date = calculated
subscription.current_due_date = calculated
subscription.due_date_source = DUE_DATE_SOURCE_RULE
return calculated
def _matching_extension_query(
*,
tenant_id: int,
catalogue_id: int,
rule_id: int | None,
financial_year: str,
assessment_year: str | None,
period_label: str | None,
):
query = select(ServiceDueDateExtension).where(
ServiceDueDateExtension.tenant_id == tenant_id,
ServiceDueDateExtension.service_catalogue_id == catalogue_id,
ServiceDueDateExtension.financial_year == financial_year,
)
if rule_id:
query = query.where(ServiceDueDateExtension.due_date_rule_id == rule_id)
if assessment_year:
query = query.where(ServiceDueDateExtension.assessment_year == assessment_year)
if period_label:
query = query.where(ServiceDueDateExtension.period_label == period_label)
else:
query = query.where((ServiceDueDateExtension.period_label.is_(None)) | (ServiceDueDateExtension.period_label == ""))
return query
def create_due_date_extension(
db: Session,
*,
tenant_id: int,
catalogue_id: int,
due_date_rule_id: int | None,
financial_year: str,
assessment_year: str | None,
period_label: str | None,
extended_due_date: date,
notification_reference: str | None,
notification_date: date | None,
remarks: str | None,
user_id: int,
) -> tuple[ServiceDueDateExtension, int, int]:
rule = get_due_rule(db, rule_id=due_date_rule_id, catalogue_id=catalogue_id) if due_date_rule_id else get_active_due_rule_for_catalogue(db, catalogue_id=catalogue_id)
latest = db.execute(
_matching_extension_query(
tenant_id=tenant_id,
catalogue_id=catalogue_id,
rule_id=rule.id if rule else None,
financial_year=financial_year,
assessment_year=assessment_year,
period_label=period_label,
).order_by(ServiceDueDateExtension.extension_sequence.desc(), ServiceDueDateExtension.id.desc())
).scalars().first()
base_due = calculate_due_date(rule, financial_year=financial_year, assessment_year=assessment_year, period_label=period_label)
previous_due = latest.extended_due_date if latest else base_due
sequence = int((latest.extension_sequence if latest else 0) or 0) + 1
extension = ServiceDueDateExtension(
tenant_id=tenant_id,
service_catalogue_id=catalogue_id,
due_date_rule_id=rule.id if rule else None,
financial_year=financial_year,
assessment_year=assessment_year,
period_label=(period_label or "").strip() or None,
previous_due_date=previous_due,
extended_due_date=extended_due_date,
extension_sequence=sequence,
notification_reference=(notification_reference or "").strip() or None,
notification_date=notification_date,
remarks=(remarks or "").strip() or None,
created_by_user_id=user_id,
updated_by_user_id=user_id,
)
db.add(extension)
db.flush()
updated = skipped_locked = 0
sub_query = select(ClientServiceSubscription).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.service_catalogue_id == catalogue_id,
ClientServiceSubscription.financial_year == financial_year,
)
if assessment_year:
sub_query = sub_query.where(ClientServiceSubscription.assessment_year == assessment_year)
subscriptions = db.execute(sub_query).scalars().all()
for sub in subscriptions:
if getattr(sub, "is_locked", False):
skipped_locked += 1
continue
if rule:
sub.due_date_rule_id = rule.id
if not getattr(sub, "original_due_date", None):
sub.original_due_date = previous_due or base_due
sub.current_due_date = extended_due_date
sub.due_date_source = DUE_DATE_SOURCE_EXTENSION
sub.updated_by_user_id = user_id
updated += 1
return extension, updated, skipped_locked