Improved reporting, price tracking

This commit is contained in:
Landon Wark
2023-02-02 14:55:49 -06:00
parent 1f832dccf2
commit d2c820f03a
13 changed files with 301 additions and 59 deletions

View File

@@ -56,6 +56,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230130.db
[post_write_hooks] [post_write_hooks]

View File

@@ -0,0 +1,40 @@
"""updating costs
Revision ID: 178203610c3b
Revises: afbdd9e46207
Create Date: 2023-02-02 09:31:05.748477
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '178203610c3b'
down_revision = 'afbdd9e46207'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_kits', schema=None) as batch_op:
batch_op.add_column(sa.Column('mutable_cost', sa.FLOAT(precision=2), nullable=True))
batch_op.add_column(sa.Column('constant_cost', sa.FLOAT(precision=2), nullable=True))
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('run_cost', sa.FLOAT(precision=2), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('run_cost')
with op.batch_alter_table('_kits', schema=None) as batch_op:
batch_op.drop_column('constant_cost')
batch_op.drop_column('mutable_cost')
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""add extraction info to submissions
Revision ID: afbdd9e46207
Revises: 8753ed70f148
Create Date: 2023-01-30 13:05:42.858306
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'afbdd9e46207'
down_revision = '8753ed70f148'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_contacts', schema=None) as batch_op:
batch_op.add_column(sa.Column('organization_id', sa.INTEGER(), nullable=True))
batch_op.create_foreign_key('fk_contact_org_id', '_organizations', ['organization_id'], ['id'], ondelete='SET NULL')
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('extraction_info', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('extraction_info')
with op.batch_alter_table('_contacts', schema=None) as batch_op:
batch_op.drop_constraint('fk_contact_org_id', type_='foreignkey')
batch_op.drop_column('organization_id')
# ### end Alembic commands ###

BIN
requirements.txt Normal file

Binary file not shown.

View File

@@ -5,7 +5,7 @@ import sqlite3
# from sqlalchemy.exc import IntegrityError, OperationalError # from sqlalchemy.exc import IntegrityError, OperationalError
# from sqlite3 import IntegrityError, OperationalError # from sqlite3 import IntegrityError, OperationalError
import logging import logging
from datetime import date, datetime from datetime import date, datetime, timedelta
from sqlalchemy import and_ from sqlalchemy import and_
import uuid import uuid
import base64 import base64
@@ -132,6 +132,12 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
except AttributeError: except AttributeError:
logger.debug(f"Could not set attribute: {item} to {info_dict[item]}") logger.debug(f"Could not set attribute: {item} to {info_dict[item]}")
continue continue
# calculate cost of the run: immutable cost + mutable times number of columns
try:
instance.run_cost = instance.extraction_kit.immutable_cost + (instance.extraction_kit.mutable_cost * ((instance.sample_count / 8)/12))
except TypeError:
logger.debug(f"Looks like that kit doesn't have cost breakdown yet, using full plate cost.")
instance.run_cost = instance.extraction_kit.cost_per_run
logger.debug(f"Constructed instance: {instance.to_string()}") logger.debug(f"Constructed instance: {instance.to_string()}")
logger.debug(msg) logger.debug(msg)
return instance, {'message':msg} return instance, {'message':msg}
@@ -395,12 +401,13 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> None:
exp (dict): Experiment dictionary created from yaml file exp (dict): Experiment dictionary created from yaml file
""" """
try: try:
super_users = ctx['super_users'] power_users = ctx['power_users']
except KeyError: except KeyError:
logger.debug("This user does not have permission to add kits.") logger.debug("This user does not have permission to add kits.")
return {'code':1,'message':"This user does not have permission to add kits."} return {'code':1,'message':"This user does not have permission to add kits."}
if getuser not in super_users: logger.debug(f"Adding kit for user: {getuser()}")
logger.debug("This user does not have permission to add kits.") if getuser() not in power_users:
logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add kits."} return {'code':1, 'message':"This user does not have permission to add kits."}
for type in exp: for type in exp:
if type == "password": if type == "password":
@@ -410,7 +417,7 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> None:
for r in exp[type]['kits'][kt]['reagenttypes']: for r in exp[type]['kits'][kt]['reagenttypes']:
look_up = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==r).first() look_up = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==r).first()
if look_up == None: if look_up == None:
rt = models.ReagentType(name=r.replace(" ", "_").lower(), eol_ext=datetime.timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), kits=[kit]) rt = models.ReagentType(name=r.replace(" ", "_").lower(), eol_ext=timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), kits=[kit])
else: else:
rt = look_up rt = look_up
rt.kits.append(kit) rt.kits.append(kit)

View File

@@ -18,6 +18,8 @@ class KitType(Base):
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
used_for = Column(JSON) #: list of names of sample types this kit can process used_for = Column(JSON) #: list of names of sample types this kit can process
cost_per_run = Column(FLOAT(2)) #: dollar amount for each full run of this kit cost_per_run = Column(FLOAT(2)) #: dollar amount for each full run of this kit
mutable_cost = Column(FLOAT(2)) #: dollar amount that can change with number of columns (reagents, tips, etc)
constant_cost = Column(FLOAT(2)) #: dollar amount that will remain constant (plates, man hours, etc)
reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) #: reagent types this kit contains reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) #: reagent types this kit contains
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', use_alter=True, name="fk_KT_reagentstype_id")) #: joined reagent type id reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', use_alter=True, name="fk_KT_reagentstype_id")) #: joined reagent type id

View File

@@ -1,5 +1,5 @@
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime as dt from datetime import datetime as dt
@@ -26,6 +26,8 @@ class BasicSubmission(Base):
# Move this into custom types? # Move this into custom types?
reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) #: relationship to reagents reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) #: relationship to reagents
reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents
extraction_info = Column(JSON) #: unstructured output from the extraction table logger.
run_cost = Column(FLOAT(2))
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "basic_submission", "polymorphic_identity": "basic_submission",
@@ -73,6 +75,7 @@ class BasicSubmission(Base):
"Sample Count": self.sample_count, "Sample Count": self.sample_count,
"Extraction Kit": ext_kit, "Extraction Kit": ext_kit,
"Technician": self.technician, "Technician": self.technician,
"Cost": self.run_cost,
} }
return output return output
@@ -99,10 +102,11 @@ class BasicSubmission(Base):
except AttributeError: except AttributeError:
ext_kit = None ext_kit = None
# get extraction kit cost from nested kittype object # get extraction kit cost from nested kittype object
try: # depreciated as it will change kit cost overtime
cost = self.extraction_kit.cost_per_run # try:
except AttributeError: # cost = self.extraction_kit.cost_per_run
cost = None # except AttributeError:
# cost = None
output = { output = {
"id": self.id, "id": self.id,
"Plate Number": self.rsl_plate_num, "Plate Number": self.rsl_plate_num,
@@ -112,7 +116,7 @@ class BasicSubmission(Base):
"Submitting Lab": sub_lab, "Submitting Lab": sub_lab,
"Sample Count": self.sample_count, "Sample Count": self.sample_count,
"Extraction Kit": ext_kit, "Extraction Kit": ext_kit,
"Cost": cost "Cost": self.run_cost
} }
return output return output

View File

@@ -88,7 +88,31 @@ class SheetParser(object):
def _parse_bacterial_culture(self) -> None: def _parse_bacterial_culture(self) -> None:
""" """
pulls info specific to bacterial culture sample type pulls info specific to bacterial culture sample type
""" """
def _parse_reagents(df:pd.DataFrame) -> None:
for ii, row in df.iterrows():
logger.debug(f"Running reagent parse for {row[1]} with type {type(row[1])} and value: {row[2]} with type {type(row[2])}")
try:
check = not np.isnan(row[1])
except TypeError:
check = True
if not isinstance(row[2], float) and check:
# must be prefixed with 'lot_' to be recognized by gui
try:
reagent_type = row[1].replace(' ', '_').lower().strip()
except AttributeError:
pass
if reagent_type == "//":
reagent_type = row[0].replace(' ', '_').lower().strip()
try:
output_var = row[2].upper()
except AttributeError:
logger.debug(f"Couldn't upperize {row[2]}, must be a number")
output_var = row[2]
logger.debug(f"Output variable is {output_var}")
self.sub[f"lot_{reagent_type}"] = output_var
submission_info = self._parse_generic("Sample List") submission_info = self._parse_generic("Sample List")
# iloc is [row][column] and the first row is set as header row so -2 # iloc is [row][column] and the first row is set as header row so -2
tech = str(submission_info.iloc[11][1]) tech = str(submission_info.iloc[11][1])
@@ -100,54 +124,91 @@ class SheetParser(object):
self.sub['technician'] = tech self.sub['technician'] = tech
# reagents # reagents
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
self.sub['lot_wash_1'] = submission_info.iloc[1][6] #if pd.isnull(submission_info.iloc[1][6]) else string_formatter(submission_info.iloc[1][6]) # Todo: find a more adaptable way to read reagents.
self.sub['lot_wash_2'] = submission_info.iloc[2][6] #if pd.isnull(submission_info.iloc[2][6]) else string_formatter(submission_info.iloc[2][6])
self.sub['lot_binding_buffer'] = submission_info.iloc[3][6] #if pd.isnull(submission_info.iloc[3][6]) else string_formatter(submission_info.iloc[3][6]) reagent_range = submission_info.iloc[1:13, 4:8]
self.sub['lot_magnetic_beads'] = submission_info.iloc[4][6] #if pd.isnull(submission_info.iloc[4][6]) else string_formatter(submission_info.iloc[4][6]) _parse_reagents(reagent_range)
self.sub['lot_lysis_buffer'] = submission_info.iloc[5][6] #if np.nan(submission_info.iloc[5][6]) else string_formatter(submission_info.iloc[5][6]) # self.sub['lot_wash_1'] = submission_info.iloc[1][6] #if pd.isnull(submission_info.iloc[1][6]) else string_formatter(submission_info.iloc[1][6])
self.sub['lot_elution_buffer'] = submission_info.iloc[6][6] #if pd.isnull(submission_info.iloc[6][6]) else string_formatter(submission_info.iloc[6][6]) # self.sub['lot_wash_2'] = submission_info.iloc[2][6] #if pd.isnull(submission_info.iloc[2][6]) else string_formatter(submission_info.iloc[2][6])
self.sub['lot_isopropanol'] = submission_info.iloc[9][6] #if pd.isnull(submission_info.iloc[9][6]) else string_formatter(submission_info.iloc[9][6]) # self.sub['lot_binding_buffer'] = submission_info.iloc[3][6] #if pd.isnull(submission_info.iloc[3][6]) else string_formatter(submission_info.iloc[3][6])
self.sub['lot_ethanol'] = submission_info.iloc[10][6] #if pd.isnull(submission_info.iloc[10][6]) else string_formatter(submission_info.iloc[10][6]) # self.sub['lot_magnetic_beads'] = submission_info.iloc[4][6] #if pd.isnull(submission_info.iloc[4][6]) else string_formatter(submission_info.iloc[4][6])
self.sub['lot_positive_control'] = submission_info.iloc[103][1] #if pd.isnull(submission_info.iloc[103][1]) else string_formatter(submission_info.iloc[103][1]) # self.sub['lot_lysis_buffer'] = submission_info.iloc[5][6] #if np.nan(submission_info.iloc[5][6]) else string_formatter(submission_info.iloc[5][6])
self.sub['lot_plate'] = submission_info.iloc[12][6] #if pd.isnull(submission_info.iloc[12][6]) else string_formatter(submission_info.iloc[12][6]) # self.sub['lot_elution_buffer'] = submission_info.iloc[6][6] #if pd.isnull(submission_info.iloc[6][6]) else string_formatter(submission_info.iloc[6][6])
# self.sub['lot_isopropanol'] = submission_info.iloc[9][6] #if pd.isnull(submission_info.iloc[9][6]) else string_formatter(submission_info.iloc[9][6])
# self.sub['lot_ethanol'] = submission_info.iloc[10][6] #if pd.isnull(submission_info.iloc[10][6]) else string_formatter(submission_info.iloc[10][6])
# self.sub['lot_positive_control'] = submission_info.iloc[103][1] #if pd.isnull(submission_info.iloc[103][1]) else string_formatter(submission_info.iloc[103][1])
# self.sub['lot_plate'] = submission_info.iloc[12][6] #if pd.isnull(submission_info.iloc[12][6]) else string_formatter(submission_info.iloc[12][6])
# get individual sample info # get individual sample info
sample_parser = SampleParser(submission_info.iloc[15:111]) sample_parser = SampleParser(submission_info.iloc[15:111])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
logger.debug(f"Parser result: {self.sub}") logger.debug(f"Parser result: {self.sub}")
self.sub['samples'] = sample_parse() self.sub['samples'] = sample_parse()
def _parse_wastewater(self) -> None: def _parse_wastewater(self) -> None:
""" """
pulls info specific to wastewater sample type pulls info specific to wastewater sample type
""" """
def _parse_reagents(df:pd.DataFrame) -> None:
logger.debug(df)
for ii, row in df.iterrows():
try:
check = not np.isnan(row[5])
except TypeError:
check = True
if not isinstance(row[5], float) and check:
# must be prefixed with 'lot_' to be recognized by gui
output_key = re.sub(r"\d{1,3}%", "", row[0].replace(' ', '_').lower())
try:
output_var = row[5].upper()
except AttributeError:
logger.debug(f"Couldn't upperize {row[2]}, must be a number")
output_var = row[5]
self.sub[f"lot_{output_key}"] = output_var
# submission_info = self.xl.parse("WW Submissions (ENTER HERE)") # submission_info = self.xl.parse("WW Submissions (ENTER HERE)")
submission_info = self._parse_generic("WW Submissions (ENTER HERE)") submission_info = self._parse_generic("WW Submissions (ENTER HERE)")
enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object) enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object)
enr_reagent_range = enrichment_info.iloc[0:4, 9:20]
extraction_info = self.xl.parse("Extraction Worksheet", dtype=object) extraction_info = self.xl.parse("Extraction Worksheet", dtype=object)
ext_reagent_range = extraction_info.iloc[0:5, 9:20]
qprc_info = self.xl.parse("qPCR Worksheet", dtype=object) qprc_info = self.xl.parse("qPCR Worksheet", dtype=object)
pcr_reagent_range = qprc_info.iloc[0:5, 9:20]
self.sub['technician'] = f"Enr: {enrichment_info.columns[2]}, Ext: {extraction_info.columns[2]}, PCR: {qprc_info.columns[2]}" self.sub['technician'] = f"Enr: {enrichment_info.columns[2]}, Ext: {extraction_info.columns[2]}, PCR: {qprc_info.columns[2]}"
_parse_reagents(enr_reagent_range)
_parse_reagents(ext_reagent_range)
_parse_reagents(pcr_reagent_range)
# reagents # reagents
logger.debug(qprc_info) # logger.debug(qprc_info)
self.sub['lot_lysis_buffer'] = enrichment_info.iloc[0][14] #if pd.isnull(enrichment_info.iloc[0][14]) else string_formatter(enrichment_info.iloc[0][14]) # self.sub['lot_lysis_buffer'] = enrichment_info.iloc[0][14] #if pd.isnull(enrichment_info.iloc[0][14]) else string_formatter(enrichment_info.iloc[0][14])
self.sub['lot_proteinase_K'] = enrichment_info.iloc[1][14] #if pd.isnull(enrichment_info.iloc[1][14]) else string_formatter(enrichment_info.iloc[1][14]) # self.sub['lot_proteinase_K'] = enrichment_info.iloc[1][14] #if pd.isnull(enrichment_info.iloc[1][14]) else string_formatter(enrichment_info.iloc[1][14])
self.sub['lot_magnetic_virus_particles'] = enrichment_info.iloc[2][14] #if pd.isnull(enrichment_info.iloc[2][14]) else string_formatter(enrichment_info.iloc[2][14]) # self.sub['lot_magnetic_virus_particles'] = enrichment_info.iloc[2][14] #if pd.isnull(enrichment_info.iloc[2][14]) else string_formatter(enrichment_info.iloc[2][14])
self.sub['lot_enrichment_reagent_1'] = enrichment_info.iloc[3][14] #if pd.isnull(enrichment_info.iloc[3][14]) else string_formatter(enrichment_info.iloc[3][14]) # self.sub['lot_enrichment_reagent_1'] = enrichment_info.iloc[3][14] #if pd.isnull(enrichment_info.iloc[3][14]) else string_formatter(enrichment_info.iloc[3][14])
self.sub['lot_binding_buffer'] = extraction_info.iloc[0][14] #if pd.isnull(extraction_info.iloc[0][14]) else string_formatter(extraction_info.iloc[0][14]) # self.sub['lot_binding_buffer'] = extraction_info.iloc[0][14] #if pd.isnull(extraction_info.iloc[0][14]) else string_formatter(extraction_info.iloc[0][14])
self.sub['lot_magnetic_beads'] = extraction_info.iloc[1][14] #if pd.isnull(extraction_info.iloc[1][14]) else string_formatter(extraction_info.iloc[1][14]) # self.sub['lot_magnetic_beads'] = extraction_info.iloc[1][14] #if pd.isnull(extraction_info.iloc[1][14]) else string_formatter(extraction_info.iloc[1][14])
self.sub['lot_wash'] = extraction_info.iloc[2][14] #if pd.isnull(extraction_info.iloc[2][14]) else string_formatter(extraction_info.iloc[2][14]) # self.sub['lot_wash'] = extraction_info.iloc[2][14] #if pd.isnull(extraction_info.iloc[2][14]) else string_formatter(extraction_info.iloc[2][14])
self.sub['lot_ethanol'] = extraction_info.iloc[3][14] #if pd.isnull(extraction_info.iloc[3][14]) else string_formatter(extraction_info.iloc[3][14]) # self.sub['lot_ethanol'] = extraction_info.iloc[3][14] #if pd.isnull(extraction_info.iloc[3][14]) else string_formatter(extraction_info.iloc[3][14])
self.sub['lot_elution_buffer'] = extraction_info.iloc[4][14] #if pd.isnull(extraction_info.iloc[4][14]) else string_formatter(extraction_info.iloc[4][14]) # self.sub['lot_elution_buffer'] = extraction_info.iloc[4][14] #if pd.isnull(extraction_info.iloc[4][14]) else string_formatter(extraction_info.iloc[4][14])
self.sub['lot_master_mix'] = qprc_info.iloc[0][14] #if pd.isnull(qprc_info.iloc[0][14]) else string_formatter(qprc_info.iloc[0][14]) # self.sub['lot_master_mix'] = qprc_info.iloc[0][14] #if pd.isnull(qprc_info.iloc[0][14]) else string_formatter(qprc_info.iloc[0][14])
self.sub['lot_pre_mix_1'] = qprc_info.iloc[1][14] #if pd.isnull(qprc_info.iloc[1][14]) else string_formatter(qprc_info.iloc[1][14]) # self.sub['lot_pre_mix_1'] = qprc_info.iloc[1][14] #if pd.isnull(qprc_info.iloc[1][14]) else string_formatter(qprc_info.iloc[1][14])
self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14]) # self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14])
self.sub['lot_positive_control'] = qprc_info.iloc[3][14] #if pd.isnull(qprc_info.iloc[3][14]) else string_formatter(qprc_info.iloc[3][14]) # self.sub['lot_positive_control'] = qprc_info.iloc[3][14] #if pd.isnull(qprc_info.iloc[3][14]) else string_formatter(qprc_info.iloc[3][14])
self.sub['lot_ddh2o'] = qprc_info.iloc[4][14] #if pd.isnull(qprc_info.iloc[4][14]) else string_formatter(qprc_info.iloc[4][14]) # self.sub['lot_ddh2o'] = qprc_info.iloc[4][14] #if pd.isnull(qprc_info.iloc[4][14]) else string_formatter(qprc_info.iloc[4][14])
# gt individual sample info # gt individual sample info
sample_parser = SampleParser(submission_info.iloc[16:40]) sample_parser = SampleParser(submission_info.iloc[16:40])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
self.sub['samples'] = sample_parse() self.sub['samples'] = sample_parse()
class SampleParser(object): class SampleParser(object):
""" """

View File

@@ -1,9 +1,21 @@
import pandas as pd
from pandas import DataFrame from pandas import DataFrame, concat
import numpy as np
from backend.db import models from backend.db import models
import json import json
import logging import logging
from jinja2 import Environment, FileSystemLoader
from datetime import date
import sys
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -22,12 +34,50 @@ def make_report_xlsx(records:list[dict]) -> DataFrame:
df = df.sort_values("Submitting Lab") df = df.sort_values("Submitting Lab")
# aggregate cost and sample count columns # aggregate cost and sample count columns
df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']}) df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']})
logger.debug(df2.columns)
# apply formating to cost column # apply formating to cost column
df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format) # df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format)
return df2 return df2
def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str:
"""
generates html from the report dataframe
Args:
df (DataFrame): input dataframe generated from 'make_report_xlsx' above
start_date (date): starting date of the report period
end_date (date): ending date of the report period
Returns:
str: html string
"""
old_lab = ""
output = []
for ii, row in enumerate(df.iterrows()):
row = [item for item in row]
lab = row[0][0]
logger.debug(f"Old lab: {old_lab}, Current lab: {lab}")
kit = dict(name=row[0][1], cost=row[1][('Cost', 'sum')], plate_count=int(row[1][('Cost', 'count')]), sample_count=int(row[1][('Sample Count', 'sum')]))
if lab == old_lab:
output[ii-1]['kits'].append(kit)
output[ii-1]['total_cost'] += kit['cost']
output[ii-1]['total_samples'] += kit['sample_count']
output[ii-1]['total_plates'] += kit['plate_count']
else:
adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count'])
output.append(adder)
old_lab = lab
logger.debug(output)
dicto = {'start_date':start_date, 'end_date':end_date, 'labs':output}#, "table":table}
temp = env.get_template('summary_report.html')
html = temp.render(input=dicto)
return html
# def split_controls_dictionary(ctx:dict, input_dict) -> list[dict]: # def split_controls_dictionary(ctx:dict, input_dict) -> list[dict]:
# # this will be the date in string form # # this will be the date in string form
# dict_name = list(input_dict.keys())[0] # dict_name = list(input_dict.keys())[0]
@@ -126,3 +176,4 @@ def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -
del df[column] del df[column]
# logger.debug(df) # logger.debug(df)
return df return df

View File

@@ -226,6 +226,8 @@ def copy_settings(settings_path:Path, settings:dict) -> dict:
# if the current user is not a superuser remove the superusers entry # if the current user is not a superuser remove the superusers entry
if not getpass.getuser() in settings['super_users']: if not getpass.getuser() in settings['super_users']:
del settings['super_users'] del settings['super_users']
if not getpass.getuser() in settings['power_users']:
del settings['power_users']
with open(settings_path, 'w') as f: with open(settings_path, 'w') as f:
yaml.dump(settings, f) yaml.dump(settings, f)
return settings return settings

View File

@@ -14,6 +14,8 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from pathlib import Path from pathlib import Path
import plotly import plotly
import pandas as pd
from xhtml2pdf import pisa
# import plotly.express as px # import plotly.express as px
import yaml import yaml
@@ -25,7 +27,7 @@ from backend.db import (construct_submission_info, lookup_reagent,
get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type, get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type,
get_control_subtypes get_control_subtypes
) )
from backend.excel.reports import make_report_xlsx from backend.excel.reports import make_report_xlsx, make_report_html
import numpy import numpy
from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion
import logging import logging
@@ -362,17 +364,22 @@ class App(QMainWindow):
subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date']) subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date'])
# convert each object to dict # convert each object to dict
records = [item.report_dict() for item in subs] records = [item.report_dict() for item in subs]
# make dataframe from record dictionaries
df = make_report_xlsx(records=records) df = make_report_xlsx(records=records)
# setup filedialog to handle save location of report html = make_report_html(df=df, start_date=info['start_date'], end_date=info['end_date'])
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx").resolve().__str__() # make dataframe from record dictionaries
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0]) # df = make_report_xlsx(records=records)
# save file # # setup filedialog to handle save location of report
try: home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf").resolve().__str__()
df.to_excel(fname, engine="openpyxl") # fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0])
except PermissionError: fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
pass # logger.debug(f"report output name: {fname}")
# df.to_excel(fname, engine='openpyxl')
with open(fname, "w+b") as f:
pisa.CreatePDF(html, dest=f)
df.to_excel(fname.with_suffix(".xlsx"), engine='openpyxl')
def add_kit(self): def add_kit(self):
""" """

View File

@@ -302,11 +302,16 @@ class KitAdder(QWidget):
used_for.setEditable(True) used_for.setEditable(True)
self.grid.addWidget(used_for,3,1) self.grid.addWidget(used_for,3,1)
# set cost per run # set cost per run
self.grid.addWidget(QLabel("Cost per run:"),4,0) self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
cost = QSpinBox() cost = QSpinBox()
cost.setMinimum(0) cost.setMinimum(0)
cost.setMaximum(9999) cost.setMaximum(9999)
self.grid.addWidget(cost,4,1) self.grid.addWidget(cost,4,1)
self.grid.addWidget(QLabel("Mutable cost per full plate (tips, reagents, etc.):"),5,0)
cost = QSpinBox()
cost.setMinimum(0)
cost.setMaximum(9999)
self.grid.addWidget(cost,5,1)
# button to add additional reagent types # button to add additional reagent types
self.add_RT_btn = QPushButton("Add Reagent Type") self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn) self.grid.addWidget(self.add_RT_btn)
@@ -338,7 +343,8 @@ class KitAdder(QWidget):
yml_type[used] = {} yml_type[used] = {}
yml_type[used]['kits'] = {} yml_type[used]['kits'] = {}
yml_type[used]['kits'][info['kit_name']] = {} yml_type[used]['kits'][info['kit_name']] = {}
yml_type[used]['kits'][info['kit_name']]['cost'] = info['cost_per_run'] yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["Constant cost per full plate (plates, work hours, etc.)"]
yml_type[used]['kits'][info['kit_name']]['mutable_cost'] = info["Mutable cost per full plate (tips, reagents, etc.)"]
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
logger.debug(yml_type) logger.debug(yml_type)
# send to kit constructor # send to kit constructor
@@ -381,20 +387,19 @@ class KitAdder(QWidget):
if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox) and not isinstance(prev_item, QScrollBar): if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox) and not isinstance(prev_item, QScrollBar):
logger.debug(f"Previous: {prev_item}") logger.debug(f"Previous: {prev_item}")
logger.debug(f"Item: {item}, {item.text()}") logger.debug(f"Item: {item}, {item.text()}")
values.append(item.text()) values.append(item.text().strip())
case QComboBox(): case QComboBox():
values.append(item.currentText()) values.append(item.currentText().strip())
case QDateEdit(): case QDateEdit():
values.append(item.date().toPyDate()) values.append(item.date().toPyDate())
case QSpinBox(): case QSpinBox():
values.append(item.value()) values.append(item.value())
case ReagentTypeForm(): case ReagentTypeForm():
re_labels, re_values, _ = self.extract_form_info(item) re_labels, re_values, _ = self.extract_form_info(item)
reagent = {item[0]:item[1] for item in zip(re_labels, re_values)} reagent = {item[0]:item[1] for item in zip(re_labels, re_values)}
logger.debug(reagent) logger.debug(reagent)
# reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}} # reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}}
reagents[reagent['name']] = {'eol_ext':int(reagent['extension_of_life_(months)'])} reagents[reagent["name_(*exactly*_as_it_appears_in_the_excel_submission_form)"].strip()] = {'eol_ext':int(reagent['extension_of_life_(months)'])}
prev_item = item prev_item = item
return labels, values, reagents return labels, values, reagents
@@ -408,7 +413,7 @@ class ReagentTypeForm(QWidget):
super().__init__() super().__init__()
grid = QGridLayout() grid = QGridLayout()
self.setLayout(grid) self.setLayout(grid)
grid.addWidget(QLabel("Name:"),0,0) grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0)
reagent_getter = QComboBox() reagent_getter = QComboBox()
# lookup all reagent type names from db # lookup all reagent type names from db
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx)) reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))

View File

@@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<title>Submissions Report for {{ input['start_date'] }} - {{ input['end_date'] }}</title>
</head>
<body>
<h2>Submissions Report {{ input['start_date'] }} - {{ input['end_date'] }}</h2>
<br>
{{ input['table'] }}
<br>
{% for lab in input['labs'] %}
<h3><u>{{ lab['lab'] }}:</u></h3>
{% for kit in lab['kits'] %}
<p><b>{{ kit['name'] }}</b></p>
<p> Plates: {{ kit['plate_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
{% endfor %}
<p><b>Lab total:</b></p>
<p> Plates: {{ lab['total_plates'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
<br>
{% endfor %}
</body>
</html>