Added report tab with HTML and excel export.

This commit is contained in:
lwark
2024-10-04 11:30:22 -05:00
parent c5470b9062
commit c89ec2b62c
12 changed files with 295 additions and 181 deletions

View File

@@ -2,6 +2,7 @@
- Reverted details exports from docx back to pdf.
- Large scale speedups for control chart construction.
- Reports are now given their own tab and can be updated in real time.
## 202409.05

View File

@@ -593,7 +593,7 @@ class BasicSubmission(BaseClass):
return
case item if item in self.jsons():
match key:
case "custom":
case "custom" | "source_plates":
existing = value
case _:
# logger.debug(f"Setting JSON attribute.")

View File

@@ -1,13 +1,11 @@
'''
Contains functions for generating summary reports
'''
from PyQt6.QtCore import QMarginsF
from PyQt6.QtGui import QPageLayout, QPageSize
from pandas import DataFrame, ExcelWriter
import logging, re
import logging
from pathlib import Path
from datetime import date, timedelta
from typing import List, Tuple, Any
from datetime import date
from typing import Tuple
from backend.db.models import BasicSubmission
from tools import jinja_template_loading, get_first_blank_df_row, \
row_map
@@ -21,10 +19,12 @@ env = jinja_template_loading()
class ReportMaker(object):
def __init__(self, start_date: date, end_date: date):
def __init__(self, start_date: date, end_date: date, organizations:list|None=None):
self.start_date = start_date
self.end_date = end_date
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date)
if organizations is not None:
self.subs = [sub for sub in self.subs if sub.submitting_lab.name in organizations]
self.detailed_df, self.summary_df = self.make_report_xlsx()
self.html = self.make_report_html(df=self.summary_df)
@@ -35,6 +35,8 @@ class ReportMaker(object):
Returns:
DataFrame: output dataframe
"""
if not self.subs:
return DataFrame(), DataFrame()
df = DataFrame.from_records([item.to_dict(report=True) for item in self.subs])
# NOTE: put submissions with the same lab together
df = df.sort_values("submitting_lab")
@@ -100,17 +102,6 @@ class ReportMaker(object):
if isinstance(filename, str):
filename = Path(filename)
filename = filename.absolute()
# NOTE: html_to_pdf doesn't function without a PyQt6 app
# if isinstance(obj, QWidget):
# logger.info(f"We're in PyQt environment, writing PDF to: {filename}")
# page_layout = QPageLayout()
# page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
# page_layout.setOrientation(QPageLayout.Orientation.Portrait)
# page_layout.setMargins(QMarginsF(25, 25, 25, 25))
# self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout)
# else:
# logger.info("Not in PyQt. Skipping PDF writing.")
# logger.debug("Finished writing.")
self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl')
self.summary_df.to_excel(self.writer, sheet_name="Report")
self.detailed_df.to_excel(self.writer, sheet_name="Details", index=False)

View File

@@ -70,7 +70,6 @@ class RSLNamer(object):
logger.debug(f"Using string method for {filename}.")
logger.debug(f"Using regex: {regex}")
m = regex.search(filename)
print(m)
try:
submission_type = m.lastgroup
logger.debug(f"Got submission type: {submission_type}")
@@ -98,7 +97,6 @@ class RSLNamer(object):
message="Please select submission type from list below.", obj_type=SubmissionType)
if dlg.exec():
submission_type = dlg.parse_form()
print(submission_type)
submission_type = submission_type.replace("_", " ")
return submission_type

View File

@@ -24,6 +24,7 @@ from .controls_chart import ControlsViewer
from .kit_creator import KitAdder
from .submission_type_creator import SubmissionTypeAdder, SubmissionType
from .sample_search import SearchBox
from .summary import Summary
logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger")
@@ -69,7 +70,7 @@ class App(QMainWindow):
fileMenu = menuBar.addMenu("&File")
# NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports")
# reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction)
@@ -80,7 +81,7 @@ class App(QMainWindow):
fileMenu.addAction(self.yamlImportAction)
methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample)
reportMenu.addAction(self.generateReportAction)
# reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction)
@@ -102,7 +103,7 @@ class App(QMainWindow):
# logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self)
self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self)
# self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self)
self.joinExtractionAction = QAction("Link Extraction Logs")
@@ -122,7 +123,7 @@ class App(QMainWindow):
# logger.debug(f"Connecting actions...")
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
# self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout)
@@ -254,8 +255,8 @@ class AddSubForm(QWidget):
# NOTE: Add tabs
self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Add SubmissionType")
self.tabs.addTab(self.tab4, "Add Kit")
self.tabs.addTab(self.tab3, "Summary Report")
# self.tabs.addTab(self.tab4, "Add Kit")
# NOTE: Create submission adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
@@ -282,14 +283,15 @@ class AddSubForm(QWidget):
self.tab2.layout.addWidget(self.controls_viewer)
self.tab2.setLayout(self.tab2.layout)
# NOTE: create custom widget to add new tabs
ST_adder = SubmissionTypeAdder(self)
# ST_adder = SubmissionTypeAdder(self)
summary_report = Summary(self)
self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(ST_adder)
self.tab3.layout.addWidget(summary_report)
self.tab3.setLayout(self.tab3.layout)
kit_adder = KitAdder(self)
self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(kit_adder)
self.tab4.setLayout(self.tab4.layout)
# kit_adder = KitAdder(self)
# self.tab4.layout = QVBoxLayout(self)
# self.tab4.layout.addWidget(kit_adder)
# self.tab4.setLayout(self.tab4.layout)
# NOTE: add tabs to main widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)

View File

@@ -8,7 +8,7 @@ from typing import Tuple
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
QDateEdit, QLabel, QSizePolicy, QPushButton
QDateEdit, QLabel, QSizePolicy, QPushButton, QGridLayout
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control
@@ -17,7 +17,7 @@ import logging
from pandas import DataFrame
from tools import Report, Result, get_unique_values_in_df_column, Settings, report_result
from frontend.visualizations.control_charts import CustomFigure
from .misc import StartEndDatePicker
logger = logging.getLogger(f"submissions.{__name__}")
@@ -28,10 +28,10 @@ class ControlsViewer(QWidget):
self.app = self.parent().parent()
# logger.debug(f"\n\n{self.app}\n\n")
self.report = Report()
self.datepicker = ControlsDatePicker()
self.datepicker = StartEndDatePicker(default_start=-180)
self.webengineview = QWebEngineView()
# NOTE: set tab2 layout
self.layout = QVBoxLayout(self)
self.layout = QGridLayout(self)
self.control_typer = QComboBox()
# NOTE: fetch types of controls
con_types = [item.name for item in ControlType.query()]
@@ -44,18 +44,20 @@ class ControlsViewer(QWidget):
self.sub_typer = QComboBox()
self.sub_typer.setEnabled(False)
# NOTE: add widgets to tab2 layout
self.layout.addWidget(self.datepicker)
self.layout.addWidget(self.control_typer)
self.layout.addWidget(self.mode_typer)
self.layout.addWidget(self.sub_typer)
self.layout.addWidget(self.webengineview)
self.layout.addWidget(self.datepicker, 0,0,1,2)
self.save_button = QPushButton("Save Chart", parent=self)
self.layout.addWidget(self.save_button, 0,2,1,1)
self.layout.addWidget(self.control_typer, 1,0,1,3)
self.layout.addWidget(self.mode_typer, 2,0,1,3)
self.layout.addWidget(self.sub_typer, 3,0,1,3)
self.layout.addWidget(self.webengineview, 4,0,1,3)
self.setLayout(self.layout)
self.controls_getter()
self.control_typer.currentIndexChanged.connect(self.controls_getter)
self.mode_typer.currentIndexChanged.connect(self.controls_getter)
self.datepicker.start_date.dateChanged.connect(self.controls_getter)
self.datepicker.end_date.dateChanged.connect(self.controls_getter)
self.datepicker.save_button.pressed.connect(self.save_chart_function)
self.save_button.pressed.connect(self.save_chart_function)
def save_chart_function(self):
self.fig.save_figure(parent=self)
@@ -141,7 +143,7 @@ class ControlsViewer(QWidget):
# NOTE: if no data found from query set fig to none for reporting in webview
if controls is None:
fig = None
self.datepicker.save_button.setEnabled(False)
self.save_button.setEnabled(False)
else:
# NOTE: change each control to list of dictionaries
data = [control.convert_by_mode(mode=self.mode) for control in controls]
@@ -160,7 +162,7 @@ class ControlsViewer(QWidget):
# NOTE: send dataframe to chart maker
df, modes = self.prep_df(ctx=self.app.ctx, df=df)
fig = CustomFigure(df=df, ytitle=title, modes=modes, parent=self)
self.datepicker.save_button.setEnabled(True)
self.save_button.setEnabled(True)
# logger.debug(f"Updating figure...")
self.fig = fig
# NOTE: construct html for webview
@@ -200,8 +202,6 @@ class ControlsViewer(QWidget):
continue
# NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating.
df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
logger.debug(df)
logger.debug(safe)
df = df[[c for c in df.columns if c in safe]]
# NOTE: move date of sample submitted on same date as previous ahead one.
df = self.displace_date(df=df)
@@ -340,28 +340,27 @@ class ControlsViewer(QWidget):
return df
class ControlsDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# NOTE: start date is two months prior to end date by default
sixmonthsago = QDate.currentDate().addDays(-180)
self.start_date.setDate(sixmonthsago)
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.save_button = QPushButton("Save Chart", parent=self)
self.layout.addWidget(self.save_button)
def sizeHint(self) -> QSize:
return QSize(80, 20)
# class ControlsDatePicker(QWidget):
# """
# custom widget to pick start and end dates for controls graphs
# """
#
# def __init__(self) -> None:
# super().__init__()
# self.start_date = QDateEdit(calendarPopup=True)
# # NOTE: start date is two months prior to end date by default
# sixmonthsago = QDate.currentDate().addDays(-180)
# self.start_date.setDate(sixmonthsago)
# self.end_date = QDateEdit(calendarPopup=True)
# self.end_date.setDate(QDate.currentDate())
# self.layout = QHBoxLayout()
# self.layout.addWidget(QLabel("Start Date"))
# self.layout.addWidget(self.start_date)
# self.layout.addWidget(QLabel("End Date"))
# self.layout.addWidget(self.end_date)
# self.setLayout(self.layout)
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
#
#
# def sizeHint(self) -> QSize:
# return QSize(80, 20)

View File

@@ -2,17 +2,20 @@
Contains miscellaneous widgets for frontend functions
'''
from datetime import date
from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QStandardItemModel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QPushButton, QFormLayout
QDialogButtonBox, QDateEdit, QPushButton, QFormLayout, QWidget, QHBoxLayout, QSizePolicy
)
from PyQt6.QtCore import Qt, QDate
from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF, pyqtSlot, pyqtSignal, QEvent
from tools import jinja_template_loading
from backend.db.models import *
import logging
from .pop_ups import AlertPop
from .functions import select_open_file
from .functions import select_open_file, select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
@@ -23,7 +26,9 @@ class AddReagentForm(QDialog):
"""
dialog to add gather info about new reagent
"""
def __init__(self, reagent_lot:str|None=None, reagent_role: str | None=None, expiry: date | None=None, reagent_name: str | None=None) -> None:
def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
reagent_name: str | None = None) -> None:
super().__init__()
if reagent_lot is None:
reagent_lot = reagent_role
@@ -71,7 +76,8 @@ class AddReagentForm(QDialog):
self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Lot:"))
self.layout.addWidget(self.lot_input)
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
self.layout.addWidget(
QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
self.layout.addWidget(self.exp_input)
self.layout.addWidget(QLabel("Type:"))
self.layout.addWidget(self.type_input)
@@ -101,41 +107,41 @@ class AddReagentForm(QDialog):
self.name_input.addItems(list(set([item.name for item in lookup])))
class ReportDatePicker(QDialog):
"""
custom dialog to ask for report start/stop dates
"""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
# NOTE: make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# NOTE: widgets to ask for dates
self.start_date = QDateEdit(calendarPopup=True)
self.start_date.setObjectName("start_date")
self.start_date.setDate(QDate.currentDate())
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setObjectName("end_date")
self.end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self) -> dict:
"""
Converts information in this object to a dict
Returns:
dict: output dict.
"""
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
# class ReportDatePicker(QDialog):
# """
# custom dialog to ask for report start/stop dates
# """
# def __init__(self) -> None:
# super().__init__()
# self.setWindowTitle("Select Report Date Range")
# # NOTE: make confirm/reject buttons
# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
# self.buttonBox = QDialogButtonBox(QBtn)
# self.buttonBox.accepted.connect(self.accept)
# self.buttonBox.rejected.connect(self.reject)
# # NOTE: widgets to ask for dates
# self.start_date = QDateEdit(calendarPopup=True)
# self.start_date.setObjectName("start_date")
# self.start_date.setDate(QDate.currentDate())
# self.end_date = QDateEdit(calendarPopup=True)
# self.end_date.setObjectName("end_date")
# self.end_date.setDate(QDate.currentDate())
# self.layout = QVBoxLayout()
# self.layout.addWidget(QLabel("Start Date"))
# self.layout.addWidget(self.start_date)
# self.layout.addWidget(QLabel("End Date"))
# self.layout.addWidget(self.end_date)
# self.layout.addWidget(self.buttonBox)
# self.setLayout(self.layout)
#
# def parse_form(self) -> dict:
# """
# Converts information in this object to a dict
#
# Returns:
# dict: output dict.
# """
# return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
class LogParser(QDialog):
@@ -186,3 +192,56 @@ class LogParser(QDialog):
dlg = AlertPop(message=msg, status=status)
dlg.exec()
class StartEndDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self, default_start: int) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# NOTE: start date is two months prior to end date by default
default_start = QDate.currentDate().addDays(default_start)
self.start_date.setDate(default_start)
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def sizeHint(self) -> QSize:
return QSize(80, 20)
def save_pdf(obj: QWebEngineView, filename: Path):
page_layout = QPageLayout()
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
page_layout.setOrientation(QPageLayout.Orientation.Portrait)
page_layout.setMargins(QMarginsF(25, 25, 25, 25))
obj.page().printToPdf(filename.absolute().__str__(), page_layout)
# subclass
class CheckableComboBox(QComboBox):
# once there is a checkState set, it is rendered
# here we assume default Unchecked
def addItem(self, item, header: bool = False):
super(CheckableComboBox, self).addItem(item)
item: QStandardItem = self.model().item(self.count() - 1, 0)
if not header:
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Checked)
def itemChecked(self, index):
item = self.model().item(index, 0)
return item.checkState() == Qt.CheckState.Checked
def changed(self):
logger.debug("emitting updated")
self.updated.emit()

View File

@@ -10,8 +10,9 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, html_to_pdf, jinja_template_loading
from tools import is_power_user, jinja_template_loading
from .functions import select_save_file
from .misc import save_pdf
from pathlib import Path
import logging
from getpass import getuser
@@ -177,11 +178,12 @@ class SubmissionDetails(QDialog):
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf")
page_layout = QPageLayout()
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
page_layout.setOrientation(QPageLayout.Orientation.Portrait)
page_layout.setMargins(QMarginsF(25, 25, 25, 25))
self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout)
# page_layout = QPageLayout()
# page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
# page_layout.setOrientation(QPageLayout.Orientation.Portrait)
# page_layout.setMargins(QMarginsF(25, 25, 25, 25))
# self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout)
save_pdf(obj=self, filename=fname)
class SubmissionComment(QDialog):
"""

View File

@@ -10,7 +10,7 @@ from backend.db.models import BasicSubmission
from backend.excel import ReportMaker
from tools import Report, Result, report_result
from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker
# from .misc import ReportDatePicker
logger = logging.getLogger(f"submissions.{__name__}")
@@ -226,32 +226,32 @@ class SubmissionsSheet(QTableView):
report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
return report
@report_result
def generate_report(self, *args):
"""
Make a report
"""
report = Report()
result = self.generate_report_function()
report.add_result(result)
return report
def generate_report_function(self):
"""
Generate a summary of activities for a time period
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
# NOTE: ask for date ranges
dlg = ReportDatePicker()
if dlg.exec():
info = dlg.parse_form()
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx", extension="xlsx")
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
rp.write_report(filename=fname, obj=self)
return report
# @report_result
# def generate_report(self, *args):
# """
# Make a report
# """
# report = Report()
# result = self.generate_report_function()
# report.add_result(result)
# return report
#
# def generate_report_function(self):
# """
# Generate a summary of activities for a time period
#
# Args:
# obj (QMainWindow): original app window
#
# Returns:
# Tuple[QMainWindow, dict]: Collection of new main app window and result dict
# """
# report = Report()
# # NOTE: ask for date ranges
# dlg = ReportDatePicker()
# if dlg.exec():
# info = dlg.parse_form()
# fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx", extension="xlsx")
# rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
# rp.write_report(filename=fname, obj=self)
# return report

View File

@@ -0,0 +1,78 @@
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QComboBox, QLabel
from backend.db import Organization
from backend.excel import ReportMaker
from tools import Report
from .misc import StartEndDatePicker, save_pdf, CheckableComboBox
from .functions import select_save_file
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class Summary(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
self.app = self.parent().parent()
# logger.debug(f"\n\n{self.app}\n\n")
self.report = Report()
self.datepicker = StartEndDatePicker(default_start=-31)
self.webview = QWebEngineView()
self.datepicker.start_date.dateChanged.connect(self.get_report)
self.datepicker.end_date.dateChanged.connect(self.get_report)
self.layout = QGridLayout(self)
self.layout.addWidget(self.datepicker, 0, 0, 1, 2)
self.save_excel_button = QPushButton("Save Excel", parent=self)
self.save_excel_button.pressed.connect(self.save_excel)
self.save_pdf_button = QPushButton("Save PDF", parent=self)
self.save_pdf_button.pressed.connect(self.save_pdf)
self.org_select = CheckableComboBox()
self.org_select.setEditable(False)
self.org_select.addItem("Select", header=True)
for org in [org.name for org in Organization.query()]:
self.org_select.addItem(org)
self.org_select.model().itemChanged.connect(self.get_report)
# self.org_select.itemChecked.connect(self.get_report)
self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1)
self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1)
self.layout.addWidget(self.webview, 2, 0, 1, 4)
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)
self.layout.addWidget(self.org_select, 1, 1, 1, 3)
self.setLayout(self.layout)
self.get_report()
def get_report(self):
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!")
lastmonth = self.datepicker.end_date.date().addDays(-31)
# NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth)
self.get_report()
return
# NOTE: convert to python useable date objects
self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate()
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
self.webview.setHtml(self.report_obj.html)
if self.report_obj.subs:
self.save_pdf_button.setEnabled(True)
self.save_excel_button.setEnabled(True)
else:
self.save_pdf_button.setEnabled(False)
self.save_excel_button.setEnabled(False)
def save_excel(self):
fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx")
self.report_obj.write_report(fname, obj=self)
def save_pdf(self):
fname = select_save_file(obj=self,
default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}",
extension="pdf")
save_pdf(obj=self.webview, filename=fname)

View File

@@ -1,10 +1,10 @@
<!doctype html>
<html>
<head>
<title>Submissions Report for {{ input['start_date'] }} - {{ input['end_date'] }}</title>
<title>Submissions Report for {{ input['start_date'] }} to {{ input['end_date'] }}</title>
</head>
<body>
<h2>Submissions Report {{ input['start_date'] }} - {{ input['end_date'] }}</h2>
<h2>Submissions Report {{ input['start_date'] }} to {{ input['end_date'] }}</h2>
<br>
{{ input['table'] }}
<br>
@@ -12,10 +12,10 @@
<h3><u>{{ lab['lab'] }}:</u></h3>
{% for kit in lab['kits'] %}
<p><b>{{ kit['name'] }}</b></p>
<p> Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
{% endfor %}
<p><b>Lab total:</b></p>
<p> Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
<p><b><u>Lab total:</u></b></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
<br>
{% endfor %}
</body>

View File

@@ -7,7 +7,7 @@ import json
import pprint
from json import JSONDecodeError
import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
import logging, re, yaml, sys, os, stat, platform, getpass, inspect
import pandas as pd
from jinja2 import Environment, FileSystemLoader
from logging import handlers
@@ -17,22 +17,15 @@ from sqlalchemy import create_engine, text, MetaData
from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List
from PyQt6.QtGui import QPageSize
from PyQt6.QtWebEngineWidgets import QWebEngineView
from openpyxl.worksheet.worksheet import Worksheet
from PyQt6.QtPrintSupport import QPrinter
from __init__ import project_path
from configparser import ConfigParser
from tkinter import Tk # from tkinter import Tk for Python 3.x
from tkinter.filedialog import askdirectory
# from .error_messaging import parse_exception_to_message
from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError
from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
logger = logging.getLogger(f"submissions.{__name__}")
# package_dir = Path(__file__).parents[2].resolve()
# package_dir = project_path
logger.debug(f"Package dir: {project_path}")
if platform.system() == "Windows":
@@ -154,11 +147,11 @@ def check_not_nan(cell_contents) -> bool:
bool: True if cell has value, else, false.
"""
# NOTE: check for nan as a string first
exclude = ['unnamed:', 'blank', 'void']
exclude = ['unnamed:', 'blank', 'void', 'nat', 'nan', ""]
try:
if cell_contents.lower() in exclude:
cell_contents = np.nan
cell_contents = cell_contents.lower()
# cell_contents = cell_contents.lower()
except (TypeError, AttributeError):
pass
try:
@@ -166,14 +159,6 @@ def check_not_nan(cell_contents) -> bool:
cell_contents = np.nan
except TypeError as e:
pass
if cell_contents == "nat":
cell_contents = np.nan
if cell_contents == 'nan':
cell_contents = np.nan
if cell_contents is None:
cell_contents = np.nan
if str(cell_contents).lower() == "none":
cell_contents = np.nan
try:
if pd.isnull(cell_contents):
cell_contents = np.nan
@@ -899,7 +884,6 @@ def yaml_regex_creator(loader, node):
return f"(?P<{name}>RSL(?:-|_)?{abbr}(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\sA-QS-Z]|$)?R?\d?)?)"
ctx = get_config(None)