diff --git a/CHANGES.rst b/CHANGES.rst index d23feee723..97767aa999 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Changelog **Added** +- #737 Added Instrument: Metler Toledo DL55 - #730 Added Instrument: LaChat QuickChem FIA - #729 Added Instrument: Varian Vista-PRO ICP - #694 Added "Warn Min" and "Warn Max" subfields in Analysis Specifications diff --git a/bika/lims/exportimport/instruments/__init__.py b/bika/lims/exportimport/instruments/__init__.py index 12e21ae2b8..03629b792c 100644 --- a/bika/lims/exportimport/instruments/__init__.py +++ b/bika/lims/exportimport/instruments/__init__.py @@ -22,6 +22,7 @@ from alere.pima import beads, cd4 from lachat import quickchem from lifetechnologies.qubit import qubit +from metler.toledo import dl55 from biodrop.ulite import ulite from tescan.tima import tima from sysmex.xs import i500, i1000 @@ -43,6 +44,7 @@ from genexpert import genexpert from varian.vistapro import icp + __all__ = ['abaxis.vetscan.vs2', 'abbott.m2000rt.m2000rt', 'agilent.masshunter.masshunter', @@ -61,6 +63,7 @@ 'horiba.jobinyvon.icp', 'lachat.quickchem', 'lifetechnologies.qubit.qubit', + 'metler.toledo.dl55', 'myself.myinstrument', 'nuclisens.easyq', 'panalytical.omnia.axios_xrf', @@ -103,6 +106,7 @@ ['generic.two_dimension', 'TwoDimensionCSVParser'], # ['generic.xml', ''], ['horiba.jobinyvon.icp', 'HoribaJobinYvonCSVParser'], + ['metler.toledo.dl55', 'MetlerToledoDL55Parser'], ['rigaku.supermini.wxrf', 'RigakuSuperminiWXRFCSVParser'], ['rochecobas.taqman.model48', 'RocheCobasTaqmanRSFParser'], ['rochecobas.taqman.model96', 'RocheCobasTaqmanRSFParser'], diff --git a/bika/lims/exportimport/instruments/metler/__init__.py b/bika/lims/exportimport/instruments/metler/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bika/lims/exportimport/instruments/metler/toledo/__init__.py b/bika/lims/exportimport/instruments/metler/toledo/__init__.py new file mode 100644 index 0000000000..40a96afc6f --- /dev/null +++ b/bika/lims/exportimport/instruments/metler/toledo/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/bika/lims/exportimport/instruments/metler/toledo/dl55.py b/bika/lims/exportimport/instruments/metler/toledo/dl55.py new file mode 100644 index 0000000000..b7c6d4d101 --- /dev/null +++ b/bika/lims/exportimport/instruments/metler/toledo/dl55.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +""" Metler Toledo DL55 +""" +import json +import re +import traceback + +from bika.lims import api +from bika.lims import bikaMessageFactory as _ +from bika.lims.exportimport.instruments.utils import \ + (get_instrument_import_search_criteria, + get_instrument_import_override, + get_instrument_import_ar_allowed_states) +from bika.lims.exportimport.instruments.resultsimport import \ + AnalysisResultsImporter, InstrumentResultsFileParser +from openpyxl import load_workbook + +title = "Metler Toledo DL55" + + +class MetlerToledoDL55Parser(InstrumentResultsFileParser): + def __init__(self, rsf): + InstrumentResultsFileParser.__init__(self, rsf, 'XLSX') + + def parse(self): + """ parse the data + """ + + wb = load_workbook(self.getInputFile()) + sheet = wb.worksheets[0] + + sample_id = None + cnt = 0 + for row in sheet.rows: + + # sampleid is only present in first row of each sample. + # any rows above the first sample id are ignored. + if row[1].value: + sample_id = row[1].value + if not sample_id: + continue + + # If no keyword is present, this row is skipped. + if len(row) < 7 or not isinstance(row[6].value, basestring): + continue + # keyword is stripped of non-word characters + keyword = re.sub(r"\W", "", row[6].value) + + # result is floatable or error + result = row[4].value + try: + float(result) + except ValueError: + self.log( + "Error in sample '" + sample_id + "': " + "Result for '" + + keyword + "' is not a number " + "(" + result + ").") + continue + + # Compose dict for importer. No interim values, just a result. + rawdict = { + 'DefaultResult': 'Result', + 'Result': result, + } + result = rawdict[rawdict['DefaultResult']] + column_name = rawdict['DefaultResult'] + cnt += 1 + result = self.get_result(column_name, result, cnt) + rawdict[rawdict['DefaultResult']] = result + self._addRawResult(sample_id, + values={keyword: rawdict}, + override=False) + return True + + def get_result(self, column_name, result, line): + result = str(result) + if result.startswith('--') or result == '' or result == 'ND': + return 0.0 + + if api.is_floatable(result): + result = api.to_float(result) + return result > 0.0 and result or 0.0 + self.err("No valid number ${result} in column (${column_name})", + mapping={"result": result, + "column_name": column_name}, + numline=self._numline, line=line) + return + + +class Importer(AnalysisResultsImporter): + """ Importer + """ + + def __init__(self, parser, context, idsearchcriteria, override, + allowed_ar_states=None, allowed_analysis_states=None, + instrument_uid=None): + AnalysisResultsImporter.__init__(self, + parser, + context, + idsearchcriteria, + override, + allowed_ar_states, + allowed_analysis_states, + instrument_uid) + + +def Import(context, request): + """ Import Form + """ + form = request.form + # form['file'] sometimes returns a list + infile = form['instrument_results_file'][0] if \ + isinstance(form['instrument_results_file'], list) \ + else form['instrument_results_file'] + override = form['results_override'] + artoapply = form['artoapply'] + sample = form.get('sample', 'requestid') + instrument = form.get('instrument', None) + errors = [] + logs = [] + warns = [] + + # Load the most suitable parser according to file extension/options/etc... + parser = None + if not hasattr(infile, 'filename'): + errors.append(_("No file selected")) + + parser = MetlerToledoDL55Parser(infile) + status = get_instrument_import_ar_allowed_states(artoapply) + over = get_instrument_import_override(override) + sam = get_instrument_import_search_criteria(sample) + + importer = Importer(parser=parser, + context=context, + idsearchcriteria=sam, + allowed_ar_states=status, + allowed_analysis_states=None, + override=over, + instrument_uid=instrument) + tbex = '' + try: + importer.process() + except: + tbex = traceback.format_exc() + errors = importer.errors + logs = importer.logs + warns = importer.warns + if tbex: + errors.append(tbex) + + results = {'errors': errors, 'log': logs, 'warns': warns} + + return json.dumps(results) diff --git a/bika/lims/tests/files/instruments/metler.toledo.dl55.xlsx b/bika/lims/tests/files/instruments/metler.toledo.dl55.xlsx new file mode 100644 index 0000000000..d48cb96a81 Binary files /dev/null and b/bika/lims/tests/files/instruments/metler.toledo.dl55.xlsx differ diff --git a/bika/lims/tests/files/metlerToledoDL55.xlsx b/bika/lims/tests/files/metlerToledoDL55.xlsx new file mode 100644 index 0000000000..f63ab4ae8a Binary files /dev/null and b/bika/lims/tests/files/metlerToledoDL55.xlsx differ