EOD and Carryover tickets added
This commit is contained in:
+6
-1
@@ -43,6 +43,11 @@ JIRA_TERMINAL_STATUSES=Cancelled,Waiting For Return,Device Return Not Needed
|
||||
# ticket newly transitions into one (comma-separated).
|
||||
CANCELLED_STATUSES=Cancelled
|
||||
# The two statuses your team uses at end-of-day close-out, depending on
|
||||
# whether ShipStation's automation also generated a return label.
|
||||
# whether ShipStation's automation also generated a return label. Reaching
|
||||
# either one moves a ticket to the Done tab and out of the active view.
|
||||
FULFILLED_STATUS_WITH_RETURN=Waiting For Return
|
||||
FULFILLED_STATUS_WITHOUT_RETURN=Device Return Not Needed
|
||||
# Daily intake cutoff (24h HH:MM). Tickets created after this time on their
|
||||
# arrival day are flagged "Past Cutoff" in the table and counted on the
|
||||
# Dashboard - they didn't get a full window to be processed same-day.
|
||||
INTAKE_CUTOFF_TIME=15:30
|
||||
|
||||
@@ -65,6 +65,31 @@ existing tickets rather than duplicating them.
|
||||
Dashboard's Carryover count is just these tickets filtered to "created
|
||||
before today."
|
||||
|
||||
## The Done pile
|
||||
|
||||
Once a ticket reaches either fulfilled status, it moves off the
|
||||
**Active Orders** tab and onto the **Done** tab automatically - the
|
||||
active view stays focused on what's still being worked. This is a
|
||||
filter, not a physical move: everything's still the same `orders`
|
||||
table, split by current status each time the view refreshes, so if a
|
||||
status ever changed back there'd be nothing to reconcile. Cancelled
|
||||
tickets stay on Active Orders (still need eyes on them) - only the two
|
||||
fulfilled statuses trigger the move. The Dashboard's "Active Orders"
|
||||
count and company breakdown only reflect what's still active;
|
||||
"Fulfilled Today" and "Arrived Past Cutoff Today" look at all of
|
||||
today's activity regardless of which tab something ended up on.
|
||||
|
||||
## Intake cutoff tracking
|
||||
|
||||
`INTAKE_CUTOFF_TIME` (default `15:30`, i.e. 3:30 PM) flags any ticket
|
||||
created after that time on its arrival day with a **Past Cutoff** "Yes"
|
||||
in the table, and the Dashboard shows how many of today's tickets
|
||||
arrived past cutoff. Since a cutoff ticket is known to spill into
|
||||
tomorrow the moment it arrives, it also counts toward **Carryover**
|
||||
immediately - not just once the date actually rolls over - unless it
|
||||
still gets fulfilled the same day despite arriving late, in which case
|
||||
it correctly drops out of both.
|
||||
|
||||
## How tracking numbers get pulled and matched
|
||||
|
||||
Confirmed against a real ShipStation payload from your queue (not
|
||||
|
||||
@@ -76,6 +76,11 @@ SETTINGS_SCHEMA: Dict[str, tuple[str, str, bool]] = {
|
||||
"Status Tracking",
|
||||
False,
|
||||
),
|
||||
"INTAKE_CUTOFF_TIME": (
|
||||
"Daily intake cutoff time, 24h HH:MM (e.g. 15:30 for 3:30 PM)",
|
||||
"Status Tracking",
|
||||
False,
|
||||
),
|
||||
}
|
||||
|
||||
DEFAULT_DB_URL = "sqlite:///orders.db"
|
||||
@@ -83,6 +88,7 @@ DEFAULT_COMPANY_SKU_MAP = "SH:Signify Health,OK:Oak Street Health"
|
||||
DEFAULT_TICKET_NUMBER_REGEX = r"\b[A-Z]{2,6}-\d{3,}\b"
|
||||
DEFAULT_FULFILLED_WITH_RETURN = "Waiting For Return"
|
||||
DEFAULT_FULFILLED_WITHOUT_RETURN = "Device Return Not Needed"
|
||||
DEFAULT_INTAKE_CUTOFF_TIME = "15:30"
|
||||
DEFAULT_TERMINAL_STATUSES = (
|
||||
f"Cancelled,{DEFAULT_FULFILLED_WITH_RETURN},{DEFAULT_FULFILLED_WITHOUT_RETURN}"
|
||||
)
|
||||
|
||||
@@ -64,6 +64,10 @@ class Order(Base):
|
||||
source_created_at = Column(DateTime, nullable=True)
|
||||
# When we pulled it into this app
|
||||
imported_at = Column(DateTime, nullable=False, default=dt.datetime.utcnow)
|
||||
# When this ticket first transitioned into a fulfilled status - used
|
||||
# for "fulfilled today" counts and as a rough audit trail. Set once,
|
||||
# on the transition; not touched again while it stays fulfilled.
|
||||
fulfilled_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Downstream pipeline flags - useful once Odoo export is fully wired up
|
||||
fulfilled = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Daily intake-window helpers.
|
||||
|
||||
Intake runs on a schedule (8AM-3:30PM by default), so it's useful to
|
||||
know when a ticket arrived relative to the cutoff - a ticket created
|
||||
after INTAKE_CUTOFF_TIME didn't have a full window to get processed
|
||||
same-day, which helps explain carryover rather than just seeing it as
|
||||
unexplained backlog.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Optional
|
||||
|
||||
from app import config
|
||||
|
||||
|
||||
def parse_cutoff_time(raw: str) -> Optional[dt.time]:
|
||||
raw = (raw or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
hour_str, minute_str = raw.split(":")
|
||||
return dt.time(int(hour_str), int(minute_str))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def get_cutoff_time() -> Optional[dt.time]:
|
||||
return parse_cutoff_time(
|
||||
config.get("INTAKE_CUTOFF_TIME", config.DEFAULT_INTAKE_CUTOFF_TIME)
|
||||
)
|
||||
|
||||
|
||||
def is_past_cutoff(created_at: Optional[dt.datetime], cutoff: Optional[dt.time]) -> bool:
|
||||
if not created_at or not cutoff:
|
||||
return False
|
||||
return created_at.time() > cutoff
|
||||
@@ -9,6 +9,21 @@ from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
|
||||
from app import config
|
||||
from app.status_rules import parse_status_list
|
||||
|
||||
|
||||
def get_fulfilled_statuses() -> set[str]:
|
||||
"""The two statuses that mean 'done, move to the Done pile'."""
|
||||
return parse_status_list(
|
||||
",".join(
|
||||
[
|
||||
config.get("FULFILLED_STATUS_WITH_RETURN", config.DEFAULT_FULFILLED_WITH_RETURN),
|
||||
config.get(
|
||||
"FULFILLED_STATUS_WITHOUT_RETURN", config.DEFAULT_FULFILLED_WITHOUT_RETURN
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def suggest_jira_status(tracking_numbers: List[dict]) -> Optional[str]:
|
||||
|
||||
+22
-6
@@ -33,7 +33,7 @@ from app.ui.settings_dialog import SettingsDialog
|
||||
from app.ui.widgets.orders_table import OrdersTableView
|
||||
from app.ui.widgets.dashboard import DashboardWidget
|
||||
from app.ui.widgets.order_detail_dialog import OrderDetailDialog
|
||||
from app.workers import FetchOrdersWorker, load_all_orders, get_dashboard_stats
|
||||
from app.workers import FetchOrdersWorker, load_active_and_done_orders, get_dashboard_stats
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -58,8 +58,11 @@ class MainWindow(QMainWindow):
|
||||
self.dashboard = DashboardWidget()
|
||||
self.orders_table = OrdersTableView()
|
||||
self.orders_table.order_double_clicked.connect(self._on_order_double_clicked)
|
||||
self.done_table = OrdersTableView()
|
||||
self.done_table.order_double_clicked.connect(self._on_order_double_clicked)
|
||||
self.tabs.addTab(self.dashboard, "Dashboard")
|
||||
self.tabs.addTab(self.orders_table, "All Orders")
|
||||
self.tabs.addTab(self.orders_table, "Active Orders")
|
||||
self.tabs.addTab(self.done_table, "Done")
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
self.setCentralWidget(central)
|
||||
@@ -81,6 +84,7 @@ class MainWindow(QMainWindow):
|
||||
toolbar.addSeparator()
|
||||
|
||||
export_action = QAction("Export Visible Orders to Odoo CSV", self)
|
||||
export_action.setToolTip("Exports from whichever of Active Orders / Done is open")
|
||||
export_action.triggered.connect(self._on_export_clicked)
|
||||
toolbar.addAction(export_action)
|
||||
|
||||
@@ -194,7 +198,16 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.critical(self, f"{service_name.title()} import failed", message)
|
||||
|
||||
def _on_export_clicked(self) -> None:
|
||||
orders = self.orders_table.visible_orders()
|
||||
current_tab = self.tabs.currentWidget()
|
||||
if not isinstance(current_tab, OrdersTableView):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Nothing to export",
|
||||
"Switch to the Active Orders or Done tab first, then export.",
|
||||
)
|
||||
return
|
||||
|
||||
orders = current_tab.visible_orders()
|
||||
if not orders:
|
||||
QMessageBox.information(
|
||||
self, "Nothing to export", "No orders match the current filters."
|
||||
@@ -216,11 +229,14 @@ class MainWindow(QMainWindow):
|
||||
self.status_label.setText(f"Exported {count} order(s) to {filepath}")
|
||||
|
||||
def _refresh_everything(self) -> None:
|
||||
orders = load_all_orders()
|
||||
self.orders_table.set_orders(orders)
|
||||
active_orders, done_orders = load_active_and_done_orders()
|
||||
self.orders_table.set_orders(active_orders)
|
||||
self.done_table.set_orders(done_orders)
|
||||
self.dashboard.update_stats(get_dashboard_stats())
|
||||
if not hasattr(self, "_suppress_ready_status"):
|
||||
self.status_label.setText(f"{len(orders)} order(s) in local database.")
|
||||
self.status_label.setText(
|
||||
f"{len(active_orders)} active order(s), {len(done_orders)} done."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+17
-13
@@ -59,28 +59,31 @@ class DashboardWidget(QWidget):
|
||||
heading.setFont(heading_font)
|
||||
outer.addWidget(heading)
|
||||
|
||||
# Top row: overall pipeline health
|
||||
# Top row: active workload health (fulfilled tickets have moved to the Done tab)
|
||||
top_row = QHBoxLayout()
|
||||
self.total_card = StatCard("Total Orders")
|
||||
self.fulfilled_card = StatCard("Fulfilled")
|
||||
self.fulfilled_card.setStyleSheet("QLabel { color: #1a7a1a; }")
|
||||
self.total_card = StatCard("Active Orders")
|
||||
self.cancelled_card = StatCard("Cancelled")
|
||||
self.cancelled_card.setStyleSheet("QLabel { color: #b00020; }")
|
||||
self.carryover_card = StatCard("Carryover (still open from an earlier day)")
|
||||
self.carryover_card.setStyleSheet("QLabel { color: #b06a00; }")
|
||||
for card in (self.total_card, self.fulfilled_card, self.cancelled_card, self.carryover_card):
|
||||
self.past_cutoff_card = StatCard("Arrived Past Cutoff Today")
|
||||
self.past_cutoff_card.setStyleSheet("QLabel { color: #b06a00; }")
|
||||
for card in (self.total_card, self.cancelled_card, self.carryover_card, self.past_cutoff_card):
|
||||
top_row.addWidget(card)
|
||||
outer.addLayout(top_row)
|
||||
|
||||
# Second row: tracking pull progress, since that's the 5PM task
|
||||
outer.addWidget(self._section_label("Tracking Numbers"))
|
||||
tracking_row = QHBoxLayout()
|
||||
self.tracking_received_card = StatCard("Tickets With Tracking Pulled")
|
||||
tracking_row.addWidget(self.tracking_received_card)
|
||||
outer.addLayout(tracking_row)
|
||||
# Second row: today's completions + tracking pull progress
|
||||
outer.addWidget(self._section_label("Today's Progress"))
|
||||
progress_row = QHBoxLayout()
|
||||
self.fulfilled_today_card = StatCard("Fulfilled Today (moved to Done)")
|
||||
self.fulfilled_today_card.setStyleSheet("QLabel { color: #1a7a1a; }")
|
||||
self.tracking_received_card = StatCard("Active Tickets With Tracking Pulled")
|
||||
progress_row.addWidget(self.fulfilled_today_card)
|
||||
progress_row.addWidget(self.tracking_received_card)
|
||||
outer.addLayout(progress_row)
|
||||
|
||||
# Third row: per-company totals (grid so adding a 3rd company later just works)
|
||||
outer.addWidget(self._section_label("By Company"))
|
||||
outer.addWidget(self._section_label("Active Orders By Company"))
|
||||
self.company_grid = QGridLayout()
|
||||
self.company_cards: dict[str, StatCard] = {}
|
||||
outer.addLayout(self.company_grid)
|
||||
@@ -97,9 +100,10 @@ class DashboardWidget(QWidget):
|
||||
|
||||
def update_stats(self, stats: dict) -> None:
|
||||
self.total_card.set_value(stats.get("total", 0))
|
||||
self.fulfilled_card.set_value(stats.get("fulfilled_count", 0))
|
||||
self.cancelled_card.set_value(stats.get("cancelled_count", 0))
|
||||
self.carryover_card.set_value(stats.get("carryover_count", 0))
|
||||
self.past_cutoff_card.set_value(stats.get("past_cutoff_today_count", 0))
|
||||
self.fulfilled_today_card.set_value(stats.get("fulfilled_today_count", 0))
|
||||
self.tracking_received_card.set_value(stats.get("tracking_received_count", 0))
|
||||
|
||||
by_company = stats.get("by_company", {})
|
||||
|
||||
@@ -28,7 +28,9 @@ from PyQt6.QtWidgets import (
|
||||
|
||||
from app import config
|
||||
from app.models import Order
|
||||
from app.schedule import get_cutoff_time, is_past_cutoff
|
||||
from app.status_rules import parse_status_list, status_in
|
||||
from app.tracking import get_fulfilled_statuses
|
||||
|
||||
CANCELLED_ROW_COLOR = QColor(255, 210, 210)
|
||||
FULFILLED_ROW_COLOR = QColor(210, 240, 210)
|
||||
@@ -40,7 +42,8 @@ COLUMNS = [
|
||||
("summary", "Summary"),
|
||||
("status", "Status"),
|
||||
("tracking_display", "Tracking #"),
|
||||
("source_created_at", "Created"),
|
||||
("created_display", "Created"),
|
||||
("past_cutoff_display", "Past Cutoff"),
|
||||
]
|
||||
|
||||
ALL_COMPANIES = "All Companies"
|
||||
@@ -55,31 +58,27 @@ def _format_tracking(tracking_numbers) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _format_created(source_created_at) -> str:
|
||||
if not source_created_at:
|
||||
return ""
|
||||
return source_created_at.strftime("%b %d, %I:%M %p").replace(" 0", " ")
|
||||
|
||||
|
||||
class OrdersTableModel(QAbstractTableModel):
|
||||
def __init__(self, orders: List[Order] | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._orders: List[Order] = orders or []
|
||||
self._cancelled_statuses: set[str] = set()
|
||||
self._fulfilled_statuses: set[str] = set()
|
||||
self._cutoff_time = None
|
||||
|
||||
def set_orders(self, orders: List[Order]) -> None:
|
||||
# Re-read status lists each refresh, in case Settings changed.
|
||||
# Re-read status lists and cutoff time each refresh, in case Settings changed.
|
||||
self._cancelled_statuses = parse_status_list(
|
||||
config.get("CANCELLED_STATUSES", config.DEFAULT_CANCELLED_STATUSES)
|
||||
)
|
||||
self._fulfilled_statuses = parse_status_list(
|
||||
",".join(
|
||||
[
|
||||
config.get(
|
||||
"FULFILLED_STATUS_WITH_RETURN", config.DEFAULT_FULFILLED_WITH_RETURN
|
||||
),
|
||||
config.get(
|
||||
"FULFILLED_STATUS_WITHOUT_RETURN",
|
||||
config.DEFAULT_FULFILLED_WITHOUT_RETURN,
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
self._fulfilled_statuses = get_fulfilled_statuses()
|
||||
self._cutoff_time = get_cutoff_time()
|
||||
self.beginResetModel()
|
||||
self._orders = orders
|
||||
self.endResetModel()
|
||||
@@ -119,6 +118,10 @@ class OrdersTableModel(QAbstractTableModel):
|
||||
return ", ".join(order.skus or [])
|
||||
if field_name == "tracking_display":
|
||||
return _format_tracking(order.tracking_numbers)
|
||||
if field_name == "created_display":
|
||||
return _format_created(order.source_created_at)
|
||||
if field_name == "past_cutoff_display":
|
||||
return "Yes" if is_past_cutoff(order.source_created_at, self._cutoff_time) else ""
|
||||
|
||||
value = getattr(order, field_name)
|
||||
return "" if value is None else str(value)
|
||||
|
||||
+73
-24
@@ -9,7 +9,7 @@ pattern rather than blocking the UI.
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import List, TypedDict
|
||||
from typing import List, Tuple, TypedDict
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
from sqlalchemy import select
|
||||
@@ -17,8 +17,10 @@ from sqlalchemy import select
|
||||
from app import config
|
||||
from app.database import get_session
|
||||
from app.models import Order
|
||||
from app.schedule import get_cutoff_time, is_past_cutoff
|
||||
from app.services.base import OrderService, NormalizedOrder
|
||||
from app.status_rules import parse_status_list, status_in
|
||||
from app.tracking import get_fulfilled_statuses
|
||||
|
||||
|
||||
class StatusChange(TypedDict):
|
||||
@@ -69,6 +71,11 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
|
||||
existing JIRA row for that ticket. If no such row exists locally yet,
|
||||
the ticket number is reported back as unmatched instead of silently
|
||||
dropped.
|
||||
|
||||
Whenever a JIRA ticket's status transitions INTO a fulfilled status
|
||||
(Waiting For Return / Device Return Not Needed), fulfilled_at is
|
||||
stamped - that's what lets the dashboard show "fulfilled today" and
|
||||
what moves it into the Done pile.
|
||||
"""
|
||||
session = get_session()
|
||||
new_count = 0
|
||||
@@ -76,6 +83,7 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
|
||||
enriched_count = 0
|
||||
status_changes: List[StatusChange] = []
|
||||
unmatched_tracking_tickets: List[str] = []
|
||||
fulfilled_statuses = get_fulfilled_statuses()
|
||||
|
||||
try:
|
||||
for order in orders:
|
||||
@@ -103,6 +111,8 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
new_status = order["status"]
|
||||
|
||||
if existing is None:
|
||||
session.add(
|
||||
Order(
|
||||
@@ -112,15 +122,17 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
|
||||
company=order.get("company", "Unknown"),
|
||||
skus=order.get("skus", []),
|
||||
summary=order["summary"],
|
||||
status=order["status"],
|
||||
status=new_status,
|
||||
source_created_at=order["source_created_at"],
|
||||
raw_data=order["raw_data"],
|
||||
fulfilled_at=(
|
||||
dt.datetime.utcnow() if status_in(new_status, fulfilled_statuses) else None
|
||||
),
|
||||
)
|
||||
)
|
||||
new_count += 1
|
||||
else:
|
||||
old_status = existing.status
|
||||
new_status = order["status"]
|
||||
if old_status != new_status:
|
||||
status_changes.append(
|
||||
StatusChange(
|
||||
@@ -129,6 +141,10 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
|
||||
new_status=new_status,
|
||||
)
|
||||
)
|
||||
if status_in(new_status, fulfilled_statuses) and not status_in(
|
||||
old_status, fulfilled_statuses
|
||||
):
|
||||
existing.fulfilled_at = dt.datetime.utcnow()
|
||||
|
||||
existing.ticket_number = order.get("ticket_number")
|
||||
existing.company = order.get("company", "Unknown")
|
||||
@@ -162,46 +178,59 @@ def load_all_orders() -> List[Order]:
|
||||
session.close()
|
||||
|
||||
|
||||
def split_active_and_done(orders: List[Order]) -> Tuple[List[Order], List[Order]]:
|
||||
"""
|
||||
Active = still being worked (includes cancelled - those still need
|
||||
eyes on them). Done = fulfilled (Waiting For Return / Device Return
|
||||
Not Needed) - archived out of the working view on purpose, per how
|
||||
the team wants to keep the active list focused on what's at hand.
|
||||
"""
|
||||
fulfilled_statuses = get_fulfilled_statuses()
|
||||
active, done = [], []
|
||||
for order in orders:
|
||||
(done if status_in(order.status, fulfilled_statuses) else active).append(order)
|
||||
return active, done
|
||||
|
||||
|
||||
def load_active_and_done_orders() -> Tuple[List[Order], List[Order]]:
|
||||
return split_active_and_done(load_all_orders())
|
||||
|
||||
|
||||
def get_dashboard_stats() -> dict:
|
||||
"""
|
||||
Counts for the dashboard: totals by company, how many are cancelled,
|
||||
fulfilled, still-open-from-a-previous-day (carryover), and how many
|
||||
have had tracking numbers pulled yet.
|
||||
Counts for the dashboard. "Active Orders" and the company breakdown
|
||||
reflect only the active workload (fulfilled tickets have moved to
|
||||
the Done pile and don't clutter this). Fulfilled-today and
|
||||
past-cutoff-today look across ALL of today's tickets regardless of
|
||||
which pile they're in now, since both are about what happened today.
|
||||
|
||||
Carryover counts any still-open ticket that's already known to spill
|
||||
into tomorrow - either it's genuinely left over from a prior day, or
|
||||
it arrived today but after the cutoff (same effect, just known a day
|
||||
earlier).
|
||||
"""
|
||||
orders = load_all_orders()
|
||||
active_orders, _done_orders = split_active_and_done(orders)
|
||||
|
||||
cancelled_statuses = parse_status_list(
|
||||
config.get("CANCELLED_STATUSES", config.DEFAULT_CANCELLED_STATUSES)
|
||||
)
|
||||
fulfilled_statuses = parse_status_list(
|
||||
",".join(
|
||||
[
|
||||
config.get("FULFILLED_STATUS_WITH_RETURN", config.DEFAULT_FULFILLED_WITH_RETURN),
|
||||
config.get(
|
||||
"FULFILLED_STATUS_WITHOUT_RETURN", config.DEFAULT_FULFILLED_WITHOUT_RETURN
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
terminal_statuses = parse_status_list(
|
||||
config.get("JIRA_TERMINAL_STATUSES", config.DEFAULT_TERMINAL_STATUSES)
|
||||
)
|
||||
|
||||
cutoff = get_cutoff_time()
|
||||
today = dt.date.today()
|
||||
|
||||
by_company: dict[str, int] = {}
|
||||
cancelled_count = 0
|
||||
fulfilled_count = 0
|
||||
carryover_count = 0
|
||||
tracking_received_count = 0
|
||||
|
||||
for order in orders:
|
||||
for order in active_orders:
|
||||
by_company[order.company] = by_company.get(order.company, 0) + 1
|
||||
|
||||
if status_in(order.status, cancelled_statuses):
|
||||
cancelled_count += 1
|
||||
if status_in(order.status, fulfilled_statuses):
|
||||
fulfilled_count += 1
|
||||
if order.tracking_numbers:
|
||||
tracking_received_count += 1
|
||||
|
||||
@@ -209,14 +238,34 @@ def get_dashboard_stats() -> dict:
|
||||
created_before_today = bool(
|
||||
order.source_created_at and order.source_created_at.date() < today
|
||||
)
|
||||
if still_open and created_before_today:
|
||||
# A ticket that arrived today but after the cutoff won't get worked
|
||||
# today either - it's known carryover before the date even rolls
|
||||
# over, not just once tomorrow arrives.
|
||||
arrived_past_cutoff_today = bool(
|
||||
order.source_created_at
|
||||
and order.source_created_at.date() == today
|
||||
and is_past_cutoff(order.source_created_at, cutoff)
|
||||
)
|
||||
if still_open and (created_before_today or arrived_past_cutoff_today):
|
||||
carryover_count += 1
|
||||
|
||||
fulfilled_today_count = sum(
|
||||
1 for o in orders if o.fulfilled_at and o.fulfilled_at.date() == today
|
||||
)
|
||||
past_cutoff_today_count = sum(
|
||||
1
|
||||
for o in orders
|
||||
if o.source_created_at
|
||||
and o.source_created_at.date() == today
|
||||
and is_past_cutoff(o.source_created_at, cutoff)
|
||||
)
|
||||
|
||||
return {
|
||||
"total": len(orders),
|
||||
"total": len(active_orders),
|
||||
"by_company": by_company,
|
||||
"cancelled_count": cancelled_count,
|
||||
"fulfilled_count": fulfilled_count,
|
||||
"carryover_count": carryover_count,
|
||||
"tracking_received_count": tracking_received_count,
|
||||
"fulfilled_today_count": fulfilled_today_count,
|
||||
"past_cutoff_today_count": past_cutoff_today_count,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user