Improved reporting, price tracking
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
40
alembic/versions/178203610c3b_updating_costs.py
Normal file
40
alembic/versions/178203610c3b_updating_costs.py
Normal 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 ###
|
||||||
@@ -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
BIN
requirements.txt
Normal file
Binary file not shown.
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,30 @@ class SheetParser(object):
|
|||||||
"""
|
"""
|
||||||
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,16 +124,20 @@ 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")
|
||||||
@@ -117,38 +145,71 @@ class SheetParser(object):
|
|||||||
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):
|
||||||
"""
|
"""
|
||||||
object to pull data for samples in excel sheet and construct individual sample objects
|
object to pull data for samples in excel sheet and construct individual sample objects
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,16 +364,21 @@ 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):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
22
src/submissions/templates/summary_report.html
Normal file
22
src/submissions/templates/summary_report.html
Normal 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>
|
||||||
Reference in New Issue
Block a user