diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..3cf82390df --- /dev/null +++ b/.travis.yml @@ -0,0 +1,33 @@ +language: python +sudo: false +cache: + pip: true + directories: + - $TRAVIS_BUILD_DIR/buildout-cache +python: + - 2.7 +addons: + apt: + packages: + - chromedriver + - chromium + - libpcre3 + - libpcre3-dev + - libssl-dev + - libexpat1-dev + - gnuplot + - libgdk-pixbuf2.0-0 +before_script: + - export DISPLAY=99.0 + - sh -e /etc/init.d/xvfb start +before_install: + - mkdir -p $TRAVIS_BUILD_DIR/buildout-cache/{eggs,downloads} + - echo "[buildout]" > $TRAVIS_BUILD_DIR/default.cfg + - echo "download-cache = $TRAVIS_BUILD_DIR/buildout-cache/downloads" >> $TRAVIS_BUILD_DIR/default.cfg + - echo "eggs-directory = $TRAVIS_BUILD_DIR/buildout-cache/eggs" >> $TRAVIS_BUILD_DIR/default.cfg + - virtualenv . + - bin/pip install --upgrade pip setuptools zc.buildout +install: + - bin/buildout -N -t 3 -c travis.cfg +script: + - bin/test -m bika.lims --layer=bika.lims.testing.BikaTestingLayer:Functional diff --git a/README.rst b/README.rst index 193b4399bd..eb7b891c4b 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Bika LIMS ========= -v3.1.11 (unreleased) +v3.2.1 (unreleased) The meaning of Gaob ------------------- diff --git a/bika/lims/browser/analyses.py b/bika/lims/browser/analyses.py index 0c7c1f578a..001cf2e9ad 100644 --- a/bika/lims/browser/analyses.py +++ b/bika/lims/browser/analyses.py @@ -12,6 +12,7 @@ from bika.lims.utils import isActive from bika.lims.utils import getUsers from bika.lims.utils import to_utf8 +from bika.lims.utils import formatDecimalMark from DateTime import DateTime from operator import itemgetter from Products.Archetypes.config import REFERENCE_CATALOG @@ -270,6 +271,8 @@ def getAnalysts(self): def folderitems(self): rc = getToolByName(self.context, REFERENCE_CATALOG) bsc = getToolByName(self.context, 'bika_setup_catalog') + analysis_categories = bsc(portal_type="AnalysisCategory", sort_on="sortable_title") + analysis_categories_order = dict([(b.Title, "{:04}".format(a)) for a, b in enumerate(analysis_categories)]) workflow = getToolByName(self.context, 'portal_workflow') mtool = getToolByName(self.context, 'portal_membership') checkPermission = mtool.checkPermission @@ -308,6 +311,7 @@ def folderitems(self): self.interim_columns = {} self.specs = {} show_methodinstr_columns = False + dmk = self.context.bika_setup.getResultsDecimalMark() for i, item in enumerate(items): # self.contentsMethod may return brains or objects. obj = hasattr(items[i]['obj'], 'getObject') and \ @@ -325,9 +329,10 @@ def folderitems(self): if self.show_categories: cat = obj.getService().getCategoryTitle() + cat_order = analysis_categories_order.get(cat) items[i]['category'] = cat - if cat not in self.categories: - self.categories.append(cat) + if (cat, cat_order) not in self.categories: + self.categories.append((cat, cat_order)) # Check for InterimFields attribute on our object, interim_fields = hasattr(obj, 'getInterimFields') \ @@ -335,7 +340,7 @@ def folderitems(self): # kick some pretty display values in. for x in range(len(interim_fields)): interim_fields[x]['formatted_value'] = \ - format_numeric_result(obj, interim_fields[x]['value']) + formatDecimalMark(interim_fields[x]['value'], dmk) self.interim_fields[obj.UID()] = interim_fields items[i]['service_uid'] = service.UID() items[i]['Service'] = service.Title() @@ -581,7 +586,6 @@ def folderitems(self): if can_view_result: items[i]['Result'] = result scinot = self.context.bika_setup.getScientificNotationResults() - dmk = self.context.bika_setup.getResultsDecimalMark() items[i]['formatted_result'] = obj.getFormattedResult(sciformat=int(scinot),decimalmark=dmk) # LIMS-1379 Allow manual uncertainty value input @@ -594,10 +598,12 @@ def folderitems(self): items[i]['Uncertainty'] = unc if unc else '' items[i]['before']['Uncertainty'] = '± '; items[i]['after']['Uncertainty'] = ' %s' % items[i]['Unit']; + items[i]['structure'] = False; elif fu: items[i]['Uncertainty'] = fu items[i]['before']['Uncertainty'] = '± '; items[i]['after']['Uncertainty'] = ' %s' % items[i]['Unit']; + items[i]['structure'] = True # LIMS-1700. Allow manual input of Detection Limits # LIMS-1775. Allow to select LDL or UDL defaults in results with readonly mode @@ -809,7 +815,10 @@ def folderitems(self): new_states.append(state) self.review_states = new_states - self.categories.sort() + if self.show_categories: + self.categories = map(lambda x: x[0], sorted(self.categories, key=lambda x: x[1])) + else: + self.categories.sort() # self.json_specs = json.dumps(self.specs) self.json_interim_fields = json.dumps(self.interim_fields) diff --git a/bika/lims/browser/analysis.py b/bika/lims/browser/analysis.py index d620cd0425..d119cb117d 100644 --- a/bika/lims/browser/analysis.py +++ b/bika/lims/browser/analysis.py @@ -2,11 +2,15 @@ from Products.CMFCore.utils import getToolByName from bika.lims.jsonapi import get_include_fields from bika.lims import bikaMessageFactory as _ +from bika.lims.browser import BrowserView from bika.lims.utils import t, dicts_to_dict +from bika.lims.utils.analysis import get_method_instrument_constraints from bika.lims.interfaces import IAnalysis, IResultOutOfRange, IJSONReadExtender from bika.lims.interfaces import IFieldIcons from bika.lims.utils import to_utf8 from bika.lims.utils import dicts_to_dict +import json +import plone from zope.component import adapts, getAdapters from zope.interface import implements @@ -29,8 +33,8 @@ def __call__(self, result=None, **kwargs): continue spec = ret["spec_values"] rngstr = "{0} {1}, {2} {3}".format( - t(_("min")), str(spec['min']), - t(_("max")), str(spec['max'])) + t(_("min")), str(spec.get('min','')), + t(_("max")), str(spec.get('max',''))) if ret["out_of_range"]: if ret["acceptable"]: message = "{0} ({1})".format( @@ -167,7 +171,7 @@ def isOutOfRange(self, result, Min, Max, error): if self.isOutOfShoulderRange(result, Min, Max, error): return True, True return True, False - + class JSONReadExtender(object): """- Adds the specification from Analysis Request to Analysis in JSON response @@ -182,7 +186,7 @@ def __init__(self, context): def analysis_specification(self): ar = self.context.aq_parent rr = dicts_to_dict(ar.getResultsRange(),'keyword') - + return rr[self.context.getService().getKeyword()] def __call__(self, request, data): @@ -193,3 +197,22 @@ def __call__(self, request, data): return data +class ajaxGetMethodInstrumentConstraints(BrowserView): + + def __call__(self): + """ + Returns a json dictionary with the constraints and rules for + methods, instruments and results to be applied to each of the + analyses specified in the request (an array of uids). + See docs/imm_results_entry_behaviour.png for further details + """ + constraints = {} + try: + plone.protect.CheckAuthenticator(self.request) + except Forbidden: + return json.dumps(constraints) + + rowuids = self.request.get('uids', '[]') + rowuids = json.loads(rowuids) + constraints = get_method_instrument_constraints(self, rowuids) + return json.dumps(constraints) diff --git a/bika/lims/browser/analysis.zcml b/bika/lims/browser/analysis.zcml index 3e010ded8a..8d331cadfb 100644 --- a/bika/lims/browser/analysis.zcml +++ b/bika/lims/browser/analysis.zcml @@ -21,9 +21,16 @@ for="bika.lims.interfaces.IAnalysis" factory="bika.lims.adapters.priorityicons.PriorityIcons" /> - - + diff --git a/bika/lims/browser/analysisrequest/analysisrequests.py b/bika/lims/browser/analysisrequest/analysisrequests.py index aabba5f4b1..8eee2abec5 100644 --- a/bika/lims/browser/analysisrequest/analysisrequests.py +++ b/bika/lims/browser/analysisrequest/analysisrequests.py @@ -4,6 +4,7 @@ from bika.lims.utils import t from bika.lims.browser.bika_listing import BikaListingView from bika.lims.utils import getUsers +from bika.lims.workflow import getTransitionDate from bika.lims.permissions import * from bika.lims.utils import to_utf8, getUsers from DateTime import DateTime @@ -106,6 +107,8 @@ def __init__(self, context, request): 'toggle': SamplingWorkflowEnabled, 'input_class': 'datepicker_nofuture', 'input_width': '10'}, + 'getDateVerified': {'title': _('Date Verified'), + 'input_width': '10'}, 'getSampler': {'title': _('Sampler'), 'toggle': SamplingWorkflowEnabled}, 'getDatePreserved': {'title': _('Date Preserved'), @@ -176,6 +179,75 @@ def __init__(self, context, request): 'getPreserver', 'getDateReceived', 'getAnalysesNum', + 'getDateVerified', + 'state_title']}, + {'id': 'to_be_sampled', + 'title': _('To Be Sampled'), + 'contentFilter': {'review_state': ('to_be_sampled',), + 'sort_on': 'created', + 'sort_order': 'reverse'}, + 'transitions': [{'id': 'sample'}, + {'id': 'submit'}, + ], + 'custom_actions': [], + 'columns': ['getRequestID', + 'getSample', + 'BatchID', + 'SubGroup', + 'Client', + 'getProfilesTitle', + 'getTemplateTitle', + 'Creator', + 'Created', + 'getClientOrderNumber', + 'getClientReference', + 'getClientSampleID', + 'ClientContact', + 'getDateSampled', + 'getSampler', + 'getDatePreserved', + 'getPreserver', + 'getSampleTypeTitle', + 'getSamplePointTitle', + 'getStorageLocation', + 'SamplingDeviation', + 'Priority', + 'AdHoc', + 'getAnalysesNum', + 'getDateVerified', + 'state_title']}, + {'id': 'to_be_preserved', + 'title': _('To Be Preserved'), + 'contentFilter': {'review_state': ('to_be_preserved',), + 'sort_on': 'created', + 'sort_order': 'reverse'}, + 'transitions': [{'id': 'preserve'},], + 'custom_actions': [], + 'columns': ['getRequestID', + 'getSample', + 'BatchID', + 'SubGroup', + 'Client', + 'getProfilesTitle', + 'getTemplateTitle', + 'Creator', + 'Created', + 'getClientOrderNumber', + 'getClientReference', + 'getClientSampleID', + 'ClientContact', + 'getDateSampled', + 'getSampler', + 'getDatePreserved', + 'getPreserver', + 'getSampleTypeTitle', + 'getSamplePointTitle', + 'getStorageLocation', + 'SamplingDeviation', + 'Priority', + 'AdHoc', + 'getAnalysesNum', + 'getDateVerified', 'state_title']}, {'id': 'sample_due', 'title': _('Due'), @@ -214,6 +286,7 @@ def __init__(self, context, request): 'Priority', 'AdHoc', 'getAnalysesNum', + 'getDateVerified', 'state_title']}, {'id': 'sample_received', 'title': _('Received'), @@ -248,6 +321,7 @@ def __init__(self, context, request): 'getDatePreserved', 'getPreserver', 'getAnalysesNum', + 'getDateVerified', 'getDateReceived']}, {'id': 'to_be_verified', 'title': _('To be verified'), @@ -284,6 +358,7 @@ def __init__(self, context, request): 'getDatePreserved', 'getPreserver', 'getAnalysesNum', + 'getDateVerified', 'getDateReceived']}, {'id': 'verified', 'title': _('Verified'), @@ -316,6 +391,7 @@ def __init__(self, context, request): 'getDatePreserved', 'getPreserver', 'getAnalysesNum', + 'getDateVerified', 'getDateReceived']}, {'id': 'published', 'title': _('Published'), @@ -349,6 +425,7 @@ def __init__(self, context, request): 'getPreserver', 'getDateReceived', 'getAnalysesNum', + 'getDateVerified', 'getDatePublished']}, {'id': 'cancelled', 'title': _('Cancelled'), @@ -393,6 +470,7 @@ def __init__(self, context, request): 'getDateReceived', 'getDatePublished', 'getAnalysesNum', + 'getDateVerified', 'state_title']}, {'id': 'invalid', 'title': _('Invalid'), @@ -426,6 +504,7 @@ def __init__(self, context, request): 'getPreserver', 'getDateReceived', 'getAnalysesNum', + 'getDateVerified', 'getDatePublished']}, {'id': 'assigned', 'title': " html tags. + # Please note the default value for the 'html' parameter from + # getFormattedResult signature is set to True, so the service will + # already take into account LDLs and UDLs symbols '<' and '>' and escape + # them if necessary. + andict['formatted_result'] = fresult; fs = '' if specs.get('min', None) and specs.get('max', None): @@ -869,7 +878,7 @@ def publishFromHTML(self, aruid, results_html): # Attach the pdf to the email if requested if pdf_report and 'pdf' in recip.get('pubpref'): - attachPdf(mime_msg, pdf_report, pdf_fn) + attachPdf(mime_msg, pdf_report, ar.id) # For now, I will simply ignore mail send under test. if hasattr(self.portal, 'robotframework'): @@ -1006,6 +1015,13 @@ def get_mail_subject(self, ar): subject = t(_('Analysis results')) return subject, tot_line + def sorted_by_sort_key(self, category_keys): + """ Sort categories via catalog lookup on title. """ + bsc = getToolByName(self.context, "bika_setup_catalog") + analysis_categories = bsc(portal_type="AnalysisCategory", sort_on="sortable_title") + sort_keys = dict([(b.Title, "{:04}".format(a)) for a, b in enumerate(analysis_categories)]) + return sorted(category_keys, key=lambda title, sk=sort_keys: sk.get(title)) + def getAnaysisBasedTransposedMatrix(self, ars): """ Returns a dict with the following structure: {'category_1_name': diff --git a/bika/lims/browser/analysisrequest/templates/reports/default.pt b/bika/lims/browser/analysisrequest/templates/reports/default.pt index 2b5c452925..93ddd363c5 100644 --- a/bika/lims/browser/analysisrequest/templates/reports/default.pt +++ b/bika/lims/browser/analysisrequest/templates/reports/default.pt @@ -208,7 +208,7 @@

Results

- + diff --git a/bika/lims/browser/analysisrequest/workflow.py b/bika/lims/browser/analysisrequest/workflow.py index f7119eb1db..6a33f1e8e8 100644 --- a/bika/lims/browser/analysisrequest/workflow.py +++ b/bika/lims/browser/analysisrequest/workflow.py @@ -294,7 +294,7 @@ def workflow_action_submit(self): if not analysis: continue # Prevent saving data if the analysis is already transitioned - if not checkPermission(EditResults, analysis): + if not (checkPermission(EditResults, analysis) or checkPermission(EditFieldResults, analysis)): title = safe_unicode(analysis.getService().Title()) msgid = _('Result for ${analysis} could not be saved because ' 'it was already submitted by another user.', diff --git a/bika/lims/browser/batchfolder.py b/bika/lims/browser/batchfolder.py index 0bd4bf22b6..e61b4a8e98 100644 --- a/bika/lims/browser/batchfolder.py +++ b/bika/lims/browser/batchfolder.py @@ -39,6 +39,7 @@ def __init__(self, context, request): 'BatchID': {'title': _('Batch ID')}, 'Description': {'title': _('Description')}, 'BatchDate': {'title': _('Date')}, + 'Client': {'title': _('Client')}, 'state_title': {'title': _('State'), 'sortable': False}, } @@ -50,6 +51,7 @@ def __init__(self, context, request): 'columns': ['Title', 'BatchID', 'BatchDate', + 'Client', 'Description', 'state_title', ] }, @@ -60,6 +62,7 @@ def __init__(self, context, request): 'columns': ['Title', 'BatchID', 'BatchDate', + 'Client', 'Description', 'state_title', ] }, @@ -70,6 +73,7 @@ def __init__(self, context, request): 'columns': ['Title', 'BatchID', 'BatchDate', + 'Client', 'Description', 'state_title', ] }, @@ -79,6 +83,7 @@ def __init__(self, context, request): 'columns': ['Title', 'BatchID', 'BatchDate', + 'Client', 'Description', 'state_title', ] }, @@ -112,6 +117,12 @@ def folderitems(self): items[x]['Title'] = title items[x]['replace']['Title'] = "%s" % (items[x]['url'], 'analysisrequests', title) + if obj.getClient(): + items[x]['Client'] = obj.getClient().Title() + items[x]['replace']['Client'] = "%s" % ( obj.getClient().absolute_url(), obj.getClient().Title()) + else: + items[x]['Client'] = '' + date = obj.Schema().getField('BatchDate').get(obj) if callable(date): date = date() diff --git a/bika/lims/browser/calcs.py b/bika/lims/browser/calcs.py index e2836eb99c..99bd55a15c 100644 --- a/bika/lims/browser/calcs.py +++ b/bika/lims/browser/calcs.py @@ -82,7 +82,7 @@ def calculate(self, uid=None): mapping = {} # values to be returned to form for this UID - Result = {'uid': uid, 'result': form_result, 'result_str': self.value} + Result = {'uid': uid, 'result': form_result} try: Result['result'] = float(form_result) except: @@ -267,34 +267,12 @@ def calculate(self, uid=None): specs = analysis.getResultsRange() # format result - belowmin = False - abovemax = False - hidemin = specs.get('hidemin', '') - hidemax = specs.get('hidemax', '') - if Result.get('result', ''): - fresult = Result['result'] - try: - belowmin = hidemin and fresult < float(hidemin) or False - except ValueError: - belowmin = False - pass - try: - abovemax = hidemax and fresult > float(hidemax) or False - except ValueError: - abovemax = False - pass - - if belowmin is True: - Result['formatted_result'] = '< %s' % hidemin - elif abovemax is True: - Result['formatted_result'] = '> %s' % hidemax - else: - try: - Result['formatted_result'] = format_numeric_result(analysis, - Result['result']) - except ValueError: - # non-float - Result['formatted_result'] = Result['result'] + try: + Result['formatted_result'] = format_numeric_result(analysis, + Result['result']) + except ValueError: + # non-float + Result['formatted_result'] = Result['result'] # calculate Dry Matter result # if parent is not an AR, it's never going to be calculable dm = hasattr(analysis.aq_parent, 'getReportDryMatter') and \ diff --git a/bika/lims/browser/fields/aranalysesfield.py b/bika/lims/browser/fields/aranalysesfield.py index d1ffd43660..6525e330ca 100644 --- a/bika/lims/browser/fields/aranalysesfield.py +++ b/bika/lims/browser/fields/aranalysesfield.py @@ -60,6 +60,7 @@ def get(self, instance, **kwargs): contentFilter = dict([(k, v) for k, v in kwargs.items() if k in bac.indexes()]) contentFilter['portal_type'] = "Analysis" + contentFilter['sort_on'] = "sortable_title" contentFilter['path'] = {'query': "/".join(instance.getPhysicalPath()), 'level': 0} analyses = bac(contentFilter) diff --git a/bika/lims/browser/invoicebatch.py b/bika/lims/browser/invoicebatch.py index 7cabbfe5b3..346959b6ba 100644 --- a/bika/lims/browser/invoicebatch.py +++ b/bika/lims/browser/invoicebatch.py @@ -14,18 +14,32 @@ def __init__(self, context, request): self.show_sort_column = False self.show_select_row = False self.show_select_all_checkbox = False - self.show_select_column = False + self.show_select_column = True self.pagesize = 25 request.set('disable_border', 1) self.context_actions = {} self.columns = { - 'id': {'title': _('Invoice Number')}, - 'client': {'title': _('Client')}, - 'invoicedate': {'title': _('Invoice Date')}, - 'subtotal': {'title': _('Subtotal')}, - 'vatamount': {'title': _('VAT')}, - 'total': {'title': _('Total')}, - } + 'id': {'title': _('Invoice Number'), + 'toggle': True }, + 'client': {'title': _('Client'), + 'toggle': True}, + 'email': {'title': _('Email Address'), + 'toggle': False}, + 'phone': {'title': _('Phone'), + 'toggle': False}, + 'invoicedate': {'title': _('Invoice Date'), + 'toggle': True}, + 'startdate': {'title': _('Start Date'), + 'toggle': False}, + 'enddate': {'title': _('End Date'), + 'toggle': False}, + 'subtotal': {'title': _('Subtotal'), + 'toggle': False}, + 'vatamount': {'title': _('VAT'), + 'toggle': False}, + 'total': {'title': _('Total'), + 'toggle': True}, + } self.review_states = [ { 'id': 'default', @@ -35,7 +49,11 @@ def __init__(self, context, request): 'columns': [ 'id', 'client', + 'email', + 'phone', 'invoicedate', + 'startdate', + 'enddate', 'subtotal', 'vatamount', 'total', @@ -69,8 +87,26 @@ def folderitems(self, full_objects=False): item['url'], obj.getId() ) item['replace']['id'] = number_link - item['client'] = obj.getClient().Title() + + if obj.getClient(): + item['client'] = obj.getClient().Title() + item['replace']['client'] = "%s" % ( + obj.getClient().absolute_url(), obj.getClient().Title() + ) + + item['email'] = obj.getClient().getEmailAddress() + item['replace']['email'] = "%s" % ( + 'mailto:%s' % obj.getClient().getEmailAddress(), obj.getClient().getEmailAddress() + ) + item['phone'] = obj.getClient().getPhone() + else: + item['client'] = '' + item['email'] = '' + item['phone'] = '' + item['invoicedate'] = self.ulocalized_time(obj.getInvoiceDate()) + item['startdate'] = self.ulocalized_time(obj.getBatchStartDate()) + item['enddate'] = self.ulocalized_time(obj.getBatchEndDate()) item['subtotal'] = currency(obj.getSubtotal()) item['vatamount'] = currency(obj.getVATAmount()) item['total'] = currency(obj.getTotal()) diff --git a/bika/lims/browser/js/bika.lims.analysisrequest.add_by_col.js b/bika/lims/browser/js/bika.lims.analysisrequest.add_by_col.js index 66c60f0fc3..9fd976339b 100644 --- a/bika/lims/browser/js/bika.lims.analysisrequest.add_by_col.js +++ b/bika/lims/browser/js/bika.lims.analysisrequest.add_by_col.js @@ -737,10 +737,10 @@ function AnalysisRequestAddByCol() { /* Configure handler for the selection of a Specification * */ - $("[id*='_Specification']") - .live('selected copy', - function (event, item) { - var arnum = $(this).parents('td').attr('arnum') + $("tr[fieldname='Specification'] td[arnum] input[type='text']") + .live('selected copy', function (event, item) { + var arnum = get_arnum(this); + state_set(arnum, 'Specification', $(this).attr('uid')); specification_refetch(arnum) }) .each(function (i, e) { @@ -820,7 +820,7 @@ function AnalysisRequestAddByCol() { var d = $.Deferred() var arnum_i = parseInt(arnum, 10) var state = bika.lims.ar_add.state[arnum_i] - var spec_uid = state['Specification_uid'] + var spec_uid = state['Specification'] if (!spec_uid) { d.resolve() return d.promise() @@ -1718,6 +1718,7 @@ function AnalysisRequestAddByCol() { var rows = $("
"+data+"
").find("tr"); $("[form_id='" + form_id + "'] tr[data-ajax_category='" + cat_title + "']").replaceWith(rows); $(element).removeClass("collapsed").addClass("expanded"); + specification_apply(); def.resolve(); }) } @@ -2163,6 +2164,11 @@ function AnalysisRequestAddByCol() { sampletype: st_uid, _authenticator: $("input[name='_authenticator']").val() } + + // HEALTH-593 Partitions not submitted when creating AR + // Disable the Add button until the partitions get calculated + $('input[name="save_button"]').prop('disabled', true); + window.jsonapi_cache = window.jsonapi_cache || {} var cacheKey = $.param(request_data) if (typeof window.jsonapi_cache[cacheKey] === "undefined") { @@ -2180,6 +2186,9 @@ function AnalysisRequestAddByCol() { window.jsonapi_cache[cacheKey] = data bika.lims.ar_add.state[arnum]['Partitions'] = data['parts'] } + // HEALTH-593 Partitions not submitted when creating AR + // Enable the Add button, partitions calculated + $('input[name="save_button"]').prop('disabled', false); d.resolve() } }) @@ -2187,6 +2196,9 @@ function AnalysisRequestAddByCol() { else { var data = window.jsonapi_cache[cacheKey] bika.lims.ar_add.state[arnum]['Partitions'] = data['parts'] + // HEALTH-593 Partitions not submitted when creating AR + // Enable the Add button, partitions calculated + $('input[name="save_button"]').prop('disabled', false); d.resolve() } return d.promise() @@ -2454,11 +2466,11 @@ function AnalysisRequestAddByCol() { window.bika.lims.portalMessage(msg) window.scroll(0, 0) } - else if (data['labels']) { + else if (data['stickers']) { var destination = window.location.href.split("/portal_factory")[0] - var ars = data['labels'] - var labelsize = data['labelsize'] - var q = "/sticker?size=" + labelsize + "&items=" + ars.join(",") + var ars = data['stickers'] + var stickertemplate = data['stickertemplate'] + var q = "/sticker?autoprint=1&template=" + stickertemplate + "&items=" + ars.join(",") window.location.replace(destination + q) } else { diff --git a/bika/lims/browser/js/bika.lims.loader.js b/bika/lims/browser/js/bika.lims.loader.js index 88e7562355..92e39ecd3c 100644 --- a/bika/lims/browser/js/bika.lims.loader.js +++ b/bika/lims/browser/js/bika.lims.loader.js @@ -78,6 +78,10 @@ window.bika.lims.controllers = { ".template-bika-lims-content-samplingsround.portaltype-client": ['ClientSamplingRoundAddEditView'], + // Sampling Rounds PrintView + "#sr_publish_container": + ['SamplingRoundPrintView'], + // Reference Samples ".portaltype-referencesample.template-analyses": ['ReferenceSampleAnalysesView'], diff --git a/bika/lims/browser/js/bika.lims.samplinground.print.js b/bika/lims/browser/js/bika.lims.samplinground.print.js new file mode 100644 index 0000000000..87b26a635a --- /dev/null +++ b/bika/lims/browser/js/bika.lims.samplinground.print.js @@ -0,0 +1,379 @@ +/** + * Controller class for SamplingRound Print View + */ +function SamplingRoundPrintView() { + + var that = this; + var referrer_cookie_name = '_srpv'; + + // Allowed Paper sizes and default margins, in mm + var papersize_default = "A4"; + var default_margins = [20, 20, 20, 20]; + var papersize = { + 'A4': { + dimensions: [210, 297], + margins: [20, 20, 20, 20] }, + + 'letter': { + dimensions: [215.9, 279.4], + margins: [20, 20, 20, 20] }, + }; + + /** + * Entry-point method for AnalysisRequestPublishView + */ + that.load = function() { + + // The report will be loaded dynamically by reloadReport() + $('#report').html('').hide(); + + // Load the report + reloadReport(); + + // Store referrer in cookie in case it is lost due to a page reload + var cookiename = "sr.publish.view.referrer"; + var backurl = document.referrer; + if (backurl) { + createCookie(cookiename, backurl); + } else { + backurl = readCookie(cookiename); + // Fallback to portal_url instead of staying inside publish. + if (!backurl) { + backurl = portal_url; + } + } + + $('#sel_format').change(function(e) { + reloadReport(); + }); + + $('#sel_layout').change(function(e) { + $('body').removeClass($('body').attr('data-layout')); + $('body').attr('data-layout', $(this).val()); + $('body').addClass($(this).val()); + reloadReport(); + }); + + $('#cancel_button').click(function(e) { + location.href=backurl; + }); + $('#print_button').click(function(e) { + e.preventDefault(); + var url = window.location.href; + $('#sr_publish_container').animate({opacity:0.4}, 'slow'); + var count = $('#sr_publish_container #report .report_body').length; + $('#sr_publish_container #report .report_body').each(function(){ + var rephtml = $(this).clone().wrap('
').parent().html(); + var repstyle = $('#report-style').clone().wrap('
').parent().html(); + repstyle += $('#layout-style').clone().wrap('
').parent().html(); + var form='
' + + '' + + '' + + '' + + '
'; + $('body').html(form); + document.forms.form.submit(); + }); + }); + }; + + function get(name){ + if(name=(new RegExp('[?&]'+encodeURIComponent(name)+'=([^&]*)')).exec(location.search)) + return decodeURIComponent(name[1]); + } + + function load_barcodes() { + // Barcode generator + $('.barcode').each(function() { + var id = $(this).attr('data-id'); + var code = $(this).attr('data-code'); + var barHeight = $(this).attr('data-barHeight'); + var addQuietZone = $(this).attr('data-addQuietZone'); + var showHRI = $(this).attr('data-showHRI'); + $(this).barcode(id, code, + {'barHeight': parseInt(barHeight), + 'addQuietZone': Boolean(addQuietZone), + 'showHRI': Boolean(showHRI) }); + }); + } + + function convert_svgs() { + $('svg').each(function(e) { + var svg = $("
").append($(this).clone()).html(); + var img = window.bika.lims.CommonUtils.svgToImage(svg); + $(this).replaceWith(img); + }); + } + + /** + * Re-load the report view in accordance to the values set in the + * options panel (report format, pagesize, QC visible, etc.) + */ + function reloadReport() { + var url = window.location.href; + var template = $('#sel_format').val(); + if ($('#report:visible').length > 0) { + $('#report').fadeTo('fast', 0.4); + } + $.ajax({ + url: url, + type: 'POST', + async: true, + data: { "template": template} + }) + .always(function(data) { + var htmldata = data; + cssdata = $(htmldata).find('#report-style').html(); + $('#report-style').html(cssdata); + htmldata = $(htmldata).find('#report').html(); + $('#report').html(htmldata); + $('#report').fadeTo('fast', 1); + load_barcodes(); + load_layout(); + convert_svgs(); + }); + } + + /** + * Applies the selected layout (A4, US-letter) to the reports view, + * splits each report in pages depending on the layout and margins + * and applies the dynamic footer and/or header if required. + * In fact, this method makes the html ready to be printed via + * Weasyprint. + */ + function load_layout() { + // Set page layout (DIN-A4, US-letter, etc.) + var currentlayout = $('#sel_layout').val(); + // Dimensions. All expressed in mm + var dim = { + size: papersize[currentlayout].size, + outerWidth: papersize[currentlayout].dimensions[0], + outerHeight: papersize[currentlayout].dimensions[1], + marginTop: papersize[currentlayout].margins[0], + marginRight: papersize[currentlayout].margins[1], + marginBottom: papersize[currentlayout].margins[2], + marginLeft: papersize[currentlayout].margins[3], + width: papersize[currentlayout].dimensions[0]-papersize[currentlayout].margins[1]-papersize[currentlayout].margins[3], + height: papersize[currentlayout].dimensions[1]-papersize[currentlayout].margins[0]-papersize[currentlayout].margins[2] + }; + + var layout_style = + '@page { size: ' + dim.size + ' !important;' + + ' width: ' + dim.width + 'mm !important;' + + ' margin: 0mm '+dim.marginRight+'mm 0mm '+dim.marginLeft+'mm !important;'; + $('#layout-style').html(layout_style); + $('#sr_publish_container').css({'width':dim.width + 'mm', 'padding': '0mm '+dim.marginRight + 'mm 0mm '+dim.marginLeft +'mm '}); + $('#sr_publish_header').css('margin', '0mm -'+dim.marginRight + 'mm 0mm -' +dim.marginLeft+'mm'); + $('div.report_body').css({'width': dim.width + 'mm', 'max-width': dim.width + 'mm', 'min-width': dim.width + 'mm'}); + + // Iterate for each AR report and apply the dimensions, header, + // footer, etc. + $('div.report_body').each(function(i) { + + var arbody = $(this); + + // Header defined for this AR Report? + // Note that if the header of the report is taller than the + // margin, the header will be dismissed. + var header_html = ''; + var header_height = $(header_html).outerHeight(true); + if ($(this).find('.page-header').length > 0) { + var pgh = $(this).find('.page-header').first(); + header_height = parseFloat($(pgh).outerHeight(true)); + if (header_height > mmTopx(dim.marginTop)) { + // Footer too tall + header_html = ""; + header_height = parseFloat($(header_html)); + } else { + header_html = ''; + } + $(this).find('.page-header').remove(); + } + + // Footer defined for this AR Report? + // Note that if the footer of the report is taller than the + // margin, the footer will be dismissed + var footer_html = ''; + var footer_height = $(footer_html).outerHeight(true); + if ($(this).find('.page-footer').length > 0) { + var pgf = $(this).find('.page-footer').first(); + footer_height = parseFloat($(pgf).outerHeight(true)); + if (footer_height > mmTopx(dim.marginBottom)) { + // Footer too tall + footer_html = ""; + footer_height = parseFloat($(footer_html)); + } else { + footer_html = ''; + } + $(this).find('.page-footer').remove(); + } + + // Remove undesired and orphan page breaks + $(this).find('.page-break').remove(); + if ($(this).find('div').last().hasClass('manual-page-break')) { + $(this).find('div').last().remove(); + } + if ($(this).find('div').first().hasClass('manual-page-break')) { + $(this).find('div').first().remove(); + } + + // Top offset by default. The position in which the report + // starts relative to the top of the window. Used later to + // calculate when a page-break is needed. + var topOffset = $(this).position().top; + var maxHeight = mmTopx(dim.height); + var elCurrent = null; + var elOutHeight = 0; + var contentHeight = 0; + var pagenum = 1; + var pagecounts = Array(); + + // Iterate through all div children to find the suitable + // page-break points, split the report and add the header + // and footer as well as pagination count as required. + // + // IMPORTANT + // Please note that only first-level div elements from + // within div.ar_publish_body are checked and will be + // treated as nob-breakable elements. So, if a div element + // from within a div.ar_publish_body is taller than the + // maximum allowed height, that element will be omitted. + // Further improvements may solve this and handle deeply + // elements from the document, such as tables, etc. Other + // elements could be then labeled with "no-break" class to + // prevent the system to break them. + //console.log("OFF\tABS\tREL\tOUT\tHEI\tMAX"); + $(this).children('div:visible').each(function(z) { + + // Is the first page? + if (elCurrent === null) { + // Add page header if required + $(header_html).insertBefore($(this)); + topOffset = $(this).position().top; + } + + // Instead of using the height css of each element to + // know if the total height at this iteration is above + // the maximum health, we use the element's position. + // This way, we will prevent underestimations due + // non-div elements or plain text directly set inside + // the div.ar_publish_body container, not wrapped by + // other div element. + var elAbsTopPos = $(this).position().top; + var elRelTopPos = elAbsTopPos - topOffset; + var elNext = $(this).next(); + elOutHeight = parseFloat($(this).outerHeight(true)); + if ($(elNext).length > 0) { + // Calculate the height of the element according to + // the position of the next element instead of + // using the outerHeight. + elOutHeight = $(elNext).position().top-elAbsTopPos; + } + + // The current element is taller than the maximum? + if (elOutHeight > maxHeight) { + console.warn("Element with id "+$(this).attr('id')+ + " has a height above the maximum: "+ + elOutHeight); + } + + // Accumulated height + contentHeight = elRelTopPos + elOutHeight; + /*console.log(Math.floor(topOffset) + "\t" + + Math.floor(elAbsTopPos) + "\t" + + Math.floor(elRelTopPos) + "\t" + + Math.floor(elOutHeight) + "\t" + + Math.floor(contentHeight) + "\t" + + Math.floor(maxHeight) + "\t" + + '#'+$(this).attr('id')+"."+$(this).attr('class'));*/ + + if (contentHeight > maxHeight || + $(this).hasClass('manual-page-break')) { + // The content is taller than the allowed height + // or a manual page break reached. Add a page break. + var paddingTopFoot = maxHeight - elRelTopPos; + var manualbreak = $(this).hasClass('manual-page-break'); + var restartcount = manualbreak && $(this).hasClass('restart-page-count'); + var aboveBreakHtml = "
"; + var pageBreak = "
"; + $(aboveBreakHtml + footer_html + pageBreak + header_html).insertBefore($(this)); + topOffset = $(this).position().top; + if (manualbreak) { + $(this).hide(); + if (restartcount) { + // The page count needs to be restarted! + pagecounts.push(pagenum); + pagenum = 0; + } + } + contentHeight = $(this).outerHeight(true); + pagenum += 1; + } + $(this).css('width', '100%'); + elCurrent = $(this); + }); + + // Document end-footer + if (elCurrent !== null) { + var paddingTopFoot = maxHeight - contentHeight; + var aboveBreakHtml = "
"; + var pageBreak = "
"; + pagecounts.push(pagenum); + $(aboveBreakHtml + footer_html + pageBreak).insertAfter($(elCurrent)); + } + + // Wrap all elements in pages + var split_at = 'div.page-header'; + $(this).find(split_at).each(function() { + $(this).add($(this).nextUntil(split_at)).wrapAll("
"); + }); + + // Move headers and footers out of the wrapping and assign + // the top and bottom margins + $(this).find('div.page-header').each(function() { + var baseheight = $(this).height(); + $(this).css({'height': pxTomm(baseheight)+"mm", + 'margin': 0, + 'padding': (pxTomm(mmTopx(dim.marginTop) - baseheight)+"mm 0 0 0")}); + $(this).parent().before(this); + }); + $(this).find('div.page-break').each(function() { + $(this).parent().after(this); + }); + $(this).find('div.page-footer').each(function() { + $(this).css({'height': dim.marginBottom+"mm", + 'margin': 0, + 'padding': 0}); + $(this).parent().after(this); + }); + + // Page numbering + pagenum = 1; + var pagecntidx = 0; + $(this).find('.page-current-num,.page-total-count,div.page-break').each(function() { + if ($(this).hasClass('page-break')) { + if ($(this).hasClass('restart-page-count')) { + pagenum = 1; + pagecntidx += 1; + } else { + pagenum = parseInt($(this).attr('data-pagenum')) + 1; + } + } else if ($(this).hasClass('page-current-num')) { + $(this).html(pagenum); + } else { + $(this).html(pagecounts[pagecntidx]); + } + }); + }); + // Remove manual page breaks + $('.manual-page-break').remove(); + } +} +var mmTopx = function(mm) { + var px = parseFloat(mm*$('#my_mm').height()); + return px > 0 ? Math.ceil(px) : Math.floor(px); +}; +var pxTomm = function(px){ + var mm = parseFloat(px/$('#my_mm').height()); + return mm > 0 ? Math.floor(mm) : Math.ceil(mm); +}; diff --git a/bika/lims/browser/js/bika.lims.utils.calcs.js b/bika/lims/browser/js/bika.lims.utils.calcs.js index ac3ff715ec..a48f8e388a 100644 --- a/bika/lims/browser/js/bika.lims.utils.calcs.js +++ b/bika/lims/browser/js/bika.lims.utils.calcs.js @@ -195,16 +195,7 @@ function CalculationUtils() { // put result values in their boxes for(i=0;i<$(data['results']).length;i++){ result = $(data['results'])[i]; - // We have to get the decimals because if they are .0, the will be ignored - var decimals = 0; - if (result.result_str.indexOf(".") > -1){ - decimals = result.result_str.split('.')[1].length; - } - var result_with_decimals = "" - if (result.result_str != ""){ - result_with_decimals = parseFloat(result.result_str).toFixed(decimals) - }; - $("input[uid='"+result.uid+"']").filter("input[field='Result']").val(result_with_decimals); + $("input[uid='"+result.uid+"']").filter("input[field='Result']").val(result.result); $('[type="hidden"]').filter("[field='ResultDM']").filter("[uid='"+result.uid+"']").val(result.dry_result); $($('[type="hidden"]').filter("[field='ResultDM']").filter("[uid='"+result.uid+"']").siblings()[0]).empty().append(result.dry_result); @@ -216,7 +207,7 @@ function CalculationUtils() { $("span[uid='"+result.uid+"']").filter("span[field='formatted_result']").empty().append(result.formatted_result); // check box - if (results != '' && results != ""){ + if (result.result != '' && result.result != ""){ if ($("[id*='cb_"+result.uid+"']").prop("checked") == false) { $("[id*='cb_"+result.uid+"']").prop('checked', true); } diff --git a/bika/lims/browser/js/bika.lims.worksheet.js b/bika/lims/browser/js/bika.lims.worksheet.js index b168456d15..4366605fa8 100644 --- a/bika/lims/browser/js/bika.lims.worksheet.js +++ b/bika/lims/browser/js/bika.lims.worksheet.js @@ -342,25 +342,159 @@ function WorksheetManageResultsView() { }); } + /** + * Stores the constraints regarding to methods and instrument assignments to + * each analysis. The variable is filled in initializeInstrumentsAndMethods + * and is used inside loadMethodEventHandlers. + */ + var mi_constraints = null; + + /** + * Applies the rules and constraints to each analysis displayed in the + * manage results view regarding to methods, instruments and results. + * For example, this service is responsible of disabling the results field + * if the analysis has no valid instrument available for the selected + * method if the service don't allow manual entry of results. Another + * example is that this service is responsible of populating the list of + * instruments avialable for an analysis service when the user changes the + * method to be used. + * See docs/imm_results_entry_behavior.png for detailed information. + */ function initializeInstrumentsAndMethods() { - var instrumentsels = $('table.bika-listing-table select.listing_select_entry[field="Instrument"]'); - $(instrumentsels).each(function() { - var sel = $(this).val(); - if ($(this).find('option[value=""]').length > 0) { - $(this).find('option[value=""]').remove(); - $(this).prepend(''); - } - $(this).val(sel); + var auids = []; + + /// Get all the analysis UIDs from this manage results table, cause + // we'll need them to retrieve all the IMM constraints/rules to be + // applied later. + var dictuids = $.parseJSON($('#lab_analyses #item_data, #analyses_form #item_data').val()); + $.each(dictuids, function(key, value) { auids.push(key); }); + + // Retrieve all the rules/constraints to be applied for each analysis + // by using an ajax call. The json dictionary returned is assigned to + // the variable mi_constraints for further use. + // FUTURE: instead of an ajax call to retrieve the dictionary, embed + // the dictionary in a div when the bika_listing template is rendered. + $.ajax({ + url: window.portal_url + "/get_method_instrument_constraints", + type: 'POST', + data: {'_authenticator': $('input[name="_authenticator"]').val(), + 'uids': $.toJSON(auids) }, + dataType: 'json' + }).done(function(data) { + // Save the constraints in the m_constraints variable + mi_constraints = data; + $.each(auids, function(index, value) { + // Apply the constraints/rules to each analysis. + load_analysis_method_constraint(value, null); + }); + }).fail(function() { + window.bika.lims.log("bika.lims.worksheet: Something went wrong while retrieving analysis-method-instrument constraints"); }); - var methodsels = $('table.bika-listing-table select.listing_select_entry[field="Method"]'); - $(methodsels).each(function() { - var sel = $(this).val(); - if ($(this).find('option[value=""]').length > 0) { - $(this).find('option[value=""]').remove(); - $(this).prepend(''); + } + + /** + * Applies the constraints and rules to the specified analysis regarding to + * the method specified. If method is null, the function assumes the rules + * must apply for the currently selected method. + * The function uses the variable mi_constraints to find out which is the + * rule to be applied to the analysis and method specified. + * See initializeInstrumentsAndMethods() function for further information + * about the constraints and rules retrieval and assignment. + * @param {string} analysis_uid - The Analysis UID + * @param {string} method_uid - The Method UID. If null, uses the method + * that is currently selected for the specified analysis. + */ + function load_analysis_method_constraint(analysis_uid, method_uid) { + if (method_uid === null) { + // Assume to load the constraints for the currently selected method + muid = $('select.listing_select_entry[field="Method"][uid="'+analysis_uid+'"]').val(); + muid = muid ? muid : ''; + load_analysis_method_constraint(analysis_uid, muid); + return; + } + andict = mi_constraints[analysis_uid]; + if (!andict) { + return; + } + constraints = andict[method_uid]; + if (!constraints || constraints.length < 7) { + return; + } + m_selector = $('select.listing_select_entry[field="Method"][uid="'+analysis_uid+'"]'); + i_selector = $('select.listing_select_entry[field="Instrument"][uid="'+analysis_uid+'"]'); + + // None option in method selector? + $(m_selector).find('option[value=""]').remove(); + if (constraints[1] == 1) { + $(m_selector).prepend(''); + } + + // Select the method + $(m_selector).val(method_uid); + + // Method selector visible? + // 0: no, 1: yes, 2: label, 3: readonly + $(m_selector).prop('disabled', false); + $('.method-label[uid="'+analysis_uid+'"]').remove(); + if (constraints[0] === 0) { + $(m_selector).hide(); + } else if (constraints[0] == 1) { + $(m_selector).show(); + } else if (constraints[0] == 2) { + if (andict.length > 1) { + $(m_selector).hide(); + var method_name = $(m_selector).find('option[value="'+method_uid+'"]').innerHtml(); + $(m_selector).after(''+method_name+''); } - $(this).val(sel); - }); + } else if (constraints[0] == 3) { + //$(m_selector).prop('disabled', true); + $(m_selector).show(); + } + + // Populate instruments list + $(i_selector).find('option').remove(); + console.log(constraints[7]); + if (constraints[7]) { + $.each(constraints[7], function(key, value) { + console.log(key+ ": "+value); + $(i_selector).append(''); + }); + } + + // None option in instrument selector? + if (constraints[3] == 1) { + $(i_selector).prepend(''); + } + + // Select the default instrument + $(i_selector).val(constraints[4]); + + // Instrument selector visible? + if (constraints[2] === 0) { + $(i_selector).hide(); + } else if (constraints[2] == 1) { + $(i_selector).show(); + } + + // Allow to edit results? + if (constraints[5] === 0) { + $('.interim input[uid="'+analysis_uid+'"]').val(''); + $('input[field="Result"][uid="'+analysis_uid+'"]').val(''); + $('.interim input[uid="'+analysis_uid+'"]').prop('disabled', true); + $('input[field="Result"][uid="'+analysis_uid+'"]').prop('disabled', true); + } else if (constraints[5] == 1) { + $('.interim input[uid="'+analysis_uid+'"]').prop('disabled', false); + $('input[field="Result"][uid="'+analysis_uid+'"]').prop('disabled', false); + } + + // Info/Warn message? + $('.alert-instruments-invalid[uid="'+analysis_uid+'"]').remove(); + if (constraints[6] && constraints[6] !== '') { + $(i_selector).after(''); + } + + $('.amconstr[uid="'+analysis_uid+'"]').remove(); + //$(m_selector).before(""+constraints[10]+"  "); } function loadHeaderEventsHandlers() { @@ -424,193 +558,9 @@ function WorksheetManageResultsView() { */ function loadMethodEventHandlers() { $('table.bika-listing-table select.listing_select_entry[field="Method"]').change(function() { - var method = null; - var service = null; - var muid = $(this).val(); var auid = $(this).attr('uid'); - var suid = $(this).attr('as_uid'); - var instrselector = $('select.listing_select_entry[field="Instrument"][uid="'+auid+'"]'); - var selectedinstr = $(instrselector).val(); - var m_manualentry = true; - var s_instrentry = false; - var qc_analysis = $(this).closest('tr').hasClass('qc-analysis'); - $(instrselector).find('option').remove(); - $(instrselector).prop('disabled', false); - $('img.alert-instruments-invalid[uid="'+auid+'"]').remove(); - $('.interim input[uid="'+auid+'"]').prop('disabled', false); - $('.input[field="Result"][uid="'+auid+'"]').prop('disabled', false); - - if (muid != '') { - // Update the instruments selector, but only if the service has AllowInstrumentEntryOfResults enabled. - // Also, only update with those instruments available for the Analysis Service. If any of the method - // instruments are available for that Analysis Service, check if the method allows the manual entry - // of results. - - // Is manual entry allowed for this method? - var request_data = { - catalog_name: "uid_catalog", - UID: muid, - include_fields: ['ManualEntryOfResultsViewField', 'Title'] - }; - window.bika.lims.jsonapi_read(request_data, function(data) { - method = (data.objects && data.objects.length > 0) ? data.objects[0] : null; - m_manualentry = (method != null) ? method.ManualEntryOfResultsViewField : true; - $('.interim input[uid="'+auid+'"]').prop('disabled', !m_manualentry); - $('.input[field="Result"][uid="'+auid+'"]').prop('disabled', !m_manualentry); - if (!m_manualentry) { - // This method doesn't allow the manual entry of Results - var title = _("Manual entry of results for method ${methodname} is not allowed", {methodname: method.Title}); - $('.input[field="Result"][uid="'+auid+'"]').parent().append(''); - } - - // Has the Analysis Service the 'Allow Instrument Entry of Results' enabled? - var request_data = { - catalog_name: "uid_catalog", - UID: suid, - include_fields: ['InstrumentEntryOfResults'] - }; - window.bika.lims.jsonapi_read(request_data, function(asdata) { - service = (asdata.objects && asdata.objects.length > 0) ? asdata.objects[0] : null; - s_instrentry = (service != null) ? service.InstrumentEntryOfResults : false; - if (!s_instrentry) { - // The service doesn't allow instrument entry of results. - // Set instrument selector to None and hide it - $(instrselector).append(""); - $(instrselector).val(''); - $(instrselector).hide(); - return; - } - - // Get the available instruments for this method and analysis service - $(instrselector).show(); - $.ajax({ - url: window.portal_url + "/get_method_service_instruments", - type: 'POST', - data: {'_authenticator': $('input[name="_authenticator"]').val(), - 'muid': muid, - 'suid': suid }, - dataType: 'json' - }).done(function(idata) { - var invalid = [] - var valid = false; - - // Populate the instrument selector with the instruments retrieved - $.each(idata, function(index, value) { - if (value['isvalid'] == true || qc_analysis == true) { - $(instrselector).append(''); - if (selectedinstr == value['uid']) { - $(instrselector).val(value['uid']) - } - valid = true; - } else { - invalid.push(value['title']) - } - }); - - if (!valid) { - // There isn't any valid instrument found - $(instrselector).append(''); - $(instrselector).val(''); - - } else if (m_manualentry) { - // Some valid instruments found and Manual Entry of Results allowed - $(instrselector).prepend(''); - - } - - if (invalid.length > 0) { - // At least one instrument is invalid (out-of-date or qc-fail) - - if (valid) { - // At least one instrument valid found too - var title = _("Invalid instruments are not shown: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - - } else if (m_manualentry) { - // All instruments found are invalid, but manual entry is allowed - var title = _("No valid instruments found: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - - } else { - // All instruments found are invalid and manual entry not allowed - var title = _("Manual entry of results for method {methodname} is not allowed and no valid instruments found: ${invalid_list}", - {methodname: method.Title, invalid_list:invalid.join(", ")}); - $(instrselector).parent().append(''); - $('.interim input[uid="'+auid+'"]').prop('disabled', true); - $('.input[field="Result"][uid="'+auid+'"]').prop('disabled', true); - } - } - - }).fail(function() { - $(instrselector).append(''); - $(instrselector).val(""); - if (!m_manualentry) { - var title = _("Unable to load instruments: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - $(instrselector).prop('disabled', true); - } else { - $(instrselector).prop('disabled', false); - } - }); - - }); - }); - - } else { - // No method selected. Which are the instruments assigned to the analysis service and without any method assigned? - $.ajax({ - url: window.portal_url + "/get_method_service_instruments", - type: 'POST', - data: {'_authenticator': $('input[name="_authenticator"]').val(), - 'muid': '0', - 'suid': suid }, - dataType: 'json' - }).done(function(idata) { - var invalid = [] - var valid = false; - - // Populate the instrument selector with the instruments retrieved - $.each(idata, function(index, value) { - if (value['isvalid'] == true) { - $(instrselector).append(''); - if (selectedinstr == value['uid']) { - $(instrselector).val(value['uid']) - } - valid = true; - } else { - invalid.push(value['title']) - } - }); - - if (!valid) { - // There isn't any valid instrument found - $(instrselector).append(''); - $(instrselector).val(''); - } else { - // Some valid instruments found and Manual Entry of Results allowed - $(instrselector).prepend(''); - } - - if (invalid.length > 0) { - // At least one instrument is invalid (out-of-date or qc-fail) - if (valid) { - // At least one instrument valid found too - var title = _("Invalid instruments are not shown: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - } else { - // All instruments found are invalid - var title = _("No valid instruments found: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - } - } - }).fail(function() { - $(instrselector).append(''); - $(instrselector).val(''); - var title = _("Unable to load instruments: ${invalid_list}", {invalid_list: invalid.join(", ")}); - $(instrselector).parent().append(''); - $(instrselector).prop('disabled', true); - }); - } + var muid = $(this).val(); + load_analysis_method_constraint(auid, muid); }); } } diff --git a/bika/lims/browser/samplinground/configure.zcml b/bika/lims/browser/samplinground/configure.zcml index 5a9a7171a8..53a13499cf 100644 --- a/bika/lims/browser/samplinground/configure.zcml +++ b/bika/lims/browser/samplinground/configure.zcml @@ -26,5 +26,12 @@ layer="bika.lims.interfaces.IBikaLIMS" /> + diff --git a/bika/lims/browser/samplinground/printform.py b/bika/lims/browser/samplinground/printform.py new file mode 100644 index 0000000000..37bf42de6b --- /dev/null +++ b/bika/lims/browser/samplinground/printform.py @@ -0,0 +1,266 @@ +from bika.lims import bikaMessageFactory as _, t +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.utils import safe_unicode +from bika.lims.utils import to_utf8, createPdf +from bika.lims.browser import BrowserView +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from plone.resource.utils import iterDirectoriesOfType, queryResourceDirectory +import App +import tempfile +import os +import glob +import traceback + + +class PrintForm(BrowserView): + template = ViewPageTemplateFile("templates/print_form.pt") + _DEFAULT_TEMPLATE = 'default_form.pt' + _TEMPLATES_DIR = 'templates/print' + _TEMPLATES_ADDON_DIR = 'samplingrounds' + _current_sr_index = 0 + _samplingrounds = [] + + def __call__(self): + if self.context.portal_type == 'SamplingRound': + self._samplingrounds = [self.context] + + elif self.context.portal_type == 'SamplingRounds' \ + and self.request.get('items', ''): + uids = self.request.get('items').split(',') + uc = getToolByName(self.context, 'uid_catalog') + self._samplingrounds = [obj.getObject() for obj in uc(UID=uids)] + + else: + # Warn and redirect to referer + logger.warning('PrintView: type not allowed: %s' % + self.context.portal_type) + self.destination_url = self.request.get_header("referer", + self.context.absolute_url()) + + # Do print? + if self.request.form.get('pdf', '0') == '1': + response = self.request.response + response.setHeader("Content-type", "application/pdf") + response.setHeader("Content-Disposition", "inline") + response.setHeader("filename", "temp.pdf") + return self.pdfFromPOST() + else: + return self.template() + + def getSamplingRoundObj(self): + """Returns the sampling round object + """ + return self.context + + def getSRTemplates(self): + """ + Returns a DisplayList with the available templates found in + browser/samplinground/templates/ + """ + this_dir = os.path.dirname(os.path.abspath(__file__)) + templates_dir = os.path.join(this_dir, self._TEMPLATES_DIR) + tempath = '%s/%s' % (templates_dir, '*.pt') + templates = [t.split('/')[-1] for t in glob.glob(tempath)] + out = [] + for template in templates: + out.append({'id': template, 'title': template[:-3]}) + for templates_resource in iterDirectoriesOfType(self._TEMPLATES_ADDON_DIR): + prefix = templates_resource.__name__ + templates = [ + tpl for tpl in templates_resource.listDirectory() + if tpl.endswith('.pt') + ] + for template in templates: + out.append({ + 'id': '{0}:{1}'.format(prefix, template), + 'title': '{0} ({1})'.format(template[:-3], prefix), + }) + return out + + def getFormTemplate(self): + """Returns the current samplinground rendered with the template + specified in the request (param 'template'). + Moves the iterator to the next samplinground available. + """ + templates_dir = self._TEMPLATES_DIR + embedt = self.request.get('template', self._DEFAULT_TEMPLATE) + if embedt.find(':') >= 0: + prefix, embedt = embedt.split(':') + templates_dir = queryResourceDirectory(self._TEMPLATES_ADDON_DIR, prefix).directory + embed = ViewPageTemplateFile(os.path.join(templates_dir, embedt)) + reptemplate = "" + try: + reptemplate = embed(self) + except: + tbex = traceback.format_exc() + wsid = self._samplingrounds[self._current_sr_index].id + reptemplate = "
%s - %s '%s':
%s
" % (wsid, _("Unable to load the template"), embedt, tbex) + if self._current_sr_index < len(self._samplingrounds): + self._current_sr_index += 1 + return reptemplate + + def getCSS(self): + """ Returns the css style to be used for the current template. + If the selected template is 'default.pt', this method will + return the content from 'default.css'. If no css file found + for the current template, returns empty string + """ + template = self.request.get('template', self._DEFAULT_TEMPLATE) + content = '' + if template.find(':') >= 0: + prefix, template = template.split(':') + resource = queryResourceDirectory( + self._TEMPLATES_ADDON_DIR, prefix) + css = '{0}.css'.format(template[:-3]) + if css in resource.listDirectory(): + content = resource.readFile(css) + else: + this_dir = os.path.dirname(os.path.abspath(__file__)) + templates_dir = os.path.join(this_dir, self._TEMPLATES_DIR) + path = '%s/%s.css' % (templates_dir, template[:-3]) + with open(path, 'r') as content_file: + content = content_file.read() + return content + + def getAnalysisRequestTemplatesInfo(self): + """ + Returns a lost of dicts with the analysis request templates infomration + [{'uid':'xxxx','id':'xxxx','title':'xxx','url':'xxx'}, ...] + """ + arts_list = [] + for art in self.context.ar_templates: + pc = getToolByName(self.context, 'portal_catalog') + contentFilter = {'portal_type': 'ARTemplate', + 'UID': art} + art_brain = pc(contentFilter) + if len(art_brain) == 1: + art_obj = art_brain[0].getObject() + arts_list.append({ + 'uid': art_obj.UID(), + 'id': art_obj.id, + 'title': art_obj.title, + 'url': art_obj.absolute_url(), + }) + return arts_list + + def getAnalysisRequestBySample(self): + """ + Returns a list of dictionaries sorted by Sample Partition/Container + [{'requests and partition info'}, ...] + """ + # rows will contain the data for each html row + rows = [] + # columns will be used to sort and define the columns + columns = { + 'column_order': [ + 'sample_id', + 'sample_type', + 'sampling_point', + 'sampling_date', + 'partition', + 'container', + 'analyses', + ], + 'titles': { + 'sample_id': _('Sample ID'), + 'sample_type': _('Sample Type'), + 'sampling_point': _('Sampling Point'), + 'sampling_date': _('Sampling Date'), + 'partition': _('Partition'), + 'container': _('Container'), + 'analyses': _('Analysis'), + } + } + ars = self.context.getAnalysisRequests() + for ar in ars: + ar = ar.getObject() + arcell = False + numans = len(ar.getAnalyses()) + for part in ar.getPartitions(): + partcell = False + container = part.getContainer().title \ + if part.getContainer() else '' + partans = part.getAnalyses() + numpartans = len(partans) + for analysis in partans: + service = analysis.getService() + row = { + 'sample_id': { + 'hidden': True if arcell else False, + 'rowspan': numans, + 'value': ar.getSample().id, + }, + 'sample_type': { + 'hidden': True if arcell else False, + 'rowspan': numans, + 'value': ar.getSampleType().title, + }, + 'sampling_point': { + 'hidden': True if arcell else False, + 'rowspan': numans, + 'value': ar.getSamplePoint().title if ar.getSamplePoint() else '', + }, + 'sampling_date': { + 'hidden': True if arcell else False, + 'rowspan': numans, + 'value': self.context.sampling_date, + }, + 'partition': { + 'hidden': True if partcell else False, + 'rowspan': numpartans, + 'value': part.id, + }, + 'container': { + 'hidden': True if partcell else False, + 'rowspan': numpartans, + 'value': container, + }, + 'analyses': { + 'title': service.title, + 'units': service.getUnit(), + }, + } + rows.append(row) + arcell = True + partcell = True + + # table will contain the data that from where the html + # will take the info + table = { + 'columns': columns, + 'rows': rows, + } + return table + + def getLab(self): + return self.context.bika_setup.laboratory.getLabURL() + + def getLogo(self): + portal = self.context.portal_url.getPortalObject() + return "%s/logo_print.png" % portal.absolute_url() + + def pdfFromPOST(self): + """ + It returns the pdf for the sampling rounds printed + """ + html = self.request.form.get('html') + style = self.request.form.get('style') + reporthtml = "%s
%s" % (style, html) + return self.printFromHTML(safe_unicode(reporthtml).encode('utf-8')) + + def printFromHTML(self, sr_html): + """ + Tis function generates a pdf file from the html + :sr_html: the html to use to generate the pdf + """ + # HTML written to debug file + debug_mode = App.config.getConfiguration().debug_mode + if debug_mode: + tmp_fn = tempfile.mktemp(suffix=".html") + open(tmp_fn, "wb").write(sr_html) + + # Creates the pdf + # we must supply the file ourself so that createPdf leaves it alone. + pdf_fn = tempfile.mktemp(suffix=".pdf") + pdf_report = createPdf(htmlreport=sr_html, outfile=pdf_fn) + return pdf_report diff --git a/bika/lims/browser/samplinground/templates/print/default_form.css b/bika/lims/browser/samplinground/templates/print/default_form.css new file mode 100644 index 0000000000..96c36c588c --- /dev/null +++ b/bika/lims/browser/samplinground/templates/print/default_form.css @@ -0,0 +1,60 @@ + +.barcode-container { + width:100%; +} +div.report_body { + font-size:0.8em; +} +div.report_body a { + color:#000; + text-decoration:none; +} +div.report_body h1 { + padding-top:15px; + font-size:1.7em; +} +div#title{ + font-size:1.5em; +} +.label { + font-weight: bold; +} +.table-text { + position: relative; +} +.data-input{ + border: 1px solid #cdcdcd; + height: 20px; +} +table.samples-grid tr td { + vertical-align:top; + border: 1px solid #cdcdcd; + padding: 5px; +} +table thead th{ + border: 1px solid #cdcdcd; + background-color: #808080; +} +#sampling-round-info div { + float: left; + font-size: 0.85em; + position: relative; + width: 150px; +} +span.units { + font-size: 0.7em; +} +div.lab-logo { + float: right; + position: relative; +} +p { + margin:0; + padding:0; +} +.clearfix { + clear:both; +} +#sampling-round-data-entry { + clear: both; +} diff --git a/bika/lims/browser/samplinground/templates/print/default_form.pt b/bika/lims/browser/samplinground/templates/print/default_form.pt new file mode 100644 index 0000000000..94eb5cc1db --- /dev/null +++ b/bika/lims/browser/samplinground/templates/print/default_form.pt @@ -0,0 +1,134 @@ + + + + + +
+
+

Description

+

+
+
+

Sampling round template

+

+ + + +

+
+

Sampler

+

+
+
+

Department

+

+ + + +

+
+
+

Sampling frequency

+

+
+
+

Sampling date

+

+
+
+

Environmental conditions

+

+
+
+

Sampling instructions

+

+
+
+

Analysis requests templates

+

+ + +
+
+

+
+
+

Sample points

+

+
+
+

Containers

+

+
+
 
+
+ +
+ + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
diff --git a/bika/lims/browser/samplinground/templates/print_form.pt b/bika/lims/browser/samplinground/templates/print_form.pt new file mode 100644 index 0000000000..833f30adb3 --- /dev/null +++ b/bika/lims/browser/samplinground/templates/print_form.pt @@ -0,0 +1,209 @@ + + + + + +
+ + +
+
+
+
+ + +
+
+ + +
+
+
+    + +
+
+ + + +
+
+
+ + +
+ + diff --git a/bika/lims/browser/stickers.py b/bika/lims/browser/stickers.py index dc55489e05..31c22a359a 100644 --- a/bika/lims/browser/stickers.py +++ b/bika/lims/browser/stickers.py @@ -18,8 +18,10 @@ class Sticker(BrowserView): template = ViewPageTemplateFile("templates/stickers_preview.pt") item_index = 0 current_item = None + rendered_items = [] def __call__(self): + self.rendered_items = [] bc = getToolByName(self.context, 'bika_catalog') items = self.request.get('items', '') if items: @@ -120,6 +122,7 @@ def getSelectedTemplate(self): bs_template = self.context.bika_setup.getLargeStickerTemplate() rq_template = self.request.get('template', bs_template) # Check if the template exists. If not, fallback to default's + prefix = '' if rq_template.find(':') >= 0: prefix, rq_template = rq_template.split(':') templates_dir = queryResourceDirectory('stickers', prefix).directory @@ -128,7 +131,7 @@ def getSelectedTemplate(self): templates_dir = os.path.join(this_dir, 'templates/stickers/') if not os.path.isfile(os.path.join(templates_dir, rq_template)): rq_template = 'Code_128_1x48mm.pt' - return rq_template + return '%s:%s' % (prefix, rq_template) if prefix else rq_template def getSelectedTemplateCSS(self): """ Looks for the CSS file from the selected template and return its @@ -161,8 +164,10 @@ def nextItem(self): first item of the list. """ if self.item_index == len(self.items): - self.item_index = 0; + self.item_index = 0 + self.rendered_items = [] self.current_item = self.items[self.item_index] + self.rendered_items.append(self.current_item[2].getId()) self.item_index += 1 return self.current_item diff --git a/bika/lims/browser/supplyorder.py b/bika/lims/browser/supplyorder.py index d4b43acd88..844d4cfbcf 100644 --- a/bika/lims/browser/supplyorder.py +++ b/bika/lims/browser/supplyorder.py @@ -82,7 +82,7 @@ def __call__(self): context.supplyorder_lineitems = [] # Process the order item data for prodid, qty in request.form.items(): - if prodid.startswith('product_') and float(qty) > 0: + if prodid.startswith('product_') and qty and float(qty) > 0: prodid = prodid.replace('product_', '') product = setup.bika_labproducts[prodid] context.supplyorder_lineitems.append( diff --git a/bika/lims/browser/templates/bika_listing_table_items.pt b/bika/lims/browser/templates/bika_listing_table_items.pt index 1170390eb2..e96949b73f 100644 --- a/bika/lims/browser/templates/bika_listing_table_items.pt +++ b/bika/lims/browser/templates/bika_listing_table_items.pt @@ -172,7 +172,7 @@ Table cells for each column from in review_state's column list. + + + + + tal:content="structure python:item['formatted_result']"/> - + + diff --git a/bika/lims/browser/templates/stickers/Code_93_2dx38mm.css b/bika/lims/browser/templates/stickers/Code_93_2dx38mm.css new file mode 100644 index 0000000000..92df0a2127 --- /dev/null +++ b/bika/lims/browser/templates/stickers/Code_93_2dx38mm.css @@ -0,0 +1,38 @@ +.sticker { + font-family: Helvetica, Arial; + font-size:6pt; + /* Total width: 3mm + 38mm + 3mm + 38mm + 3mm = 85mm*/ + width: 85mm; + height:18mm; + max-height:18mm; + max-width:85mm; + overflow:hidden; + border-bottom:none; +} +.sticker table.stickers-row { + width:100%; + border-spacing: 10px; + border-collapse: separate; + background-color:#f5f5f5; +} +.sticker table.stickers-row td.sticker-cell { + background-color:#fff; + width:38mm; + height:18mm; + max-width:38mm; + max-height:18mm; +} +.sticker .analysisrequest-info table td.sample-type { + width:100%; +} +.sticker .analysisrequest-info table, +.sticker .sampling-date-info table { + border-collapse:collapse; + font-size: 6pt; + width:100%; +} +.sticker .analysisrequest-info table td, +.sticker .sampling-date-info table td { + border:none; + padding-left:2mm; +} diff --git a/bika/lims/browser/templates/stickers/Code_93_2dx38mm.pt b/bika/lims/browser/templates/stickers/Code_93_2dx38mm.pt new file mode 100644 index 0000000000..0dbe02160c --- /dev/null +++ b/bika/lims/browser/templates/stickers/Code_93_2dx38mm.pt @@ -0,0 +1,77 @@ + + + + + + + + +
+ + + +
+
+ + +
+ + + + + +
+ + + +
+
+
+
+
diff --git a/bika/lims/browser/templates/stickers/Code_93_2x38mm.css b/bika/lims/browser/templates/stickers/Code_93_2x38mm.css new file mode 100644 index 0000000000..92df0a2127 --- /dev/null +++ b/bika/lims/browser/templates/stickers/Code_93_2x38mm.css @@ -0,0 +1,38 @@ +.sticker { + font-family: Helvetica, Arial; + font-size:6pt; + /* Total width: 3mm + 38mm + 3mm + 38mm + 3mm = 85mm*/ + width: 85mm; + height:18mm; + max-height:18mm; + max-width:85mm; + overflow:hidden; + border-bottom:none; +} +.sticker table.stickers-row { + width:100%; + border-spacing: 10px; + border-collapse: separate; + background-color:#f5f5f5; +} +.sticker table.stickers-row td.sticker-cell { + background-color:#fff; + width:38mm; + height:18mm; + max-width:38mm; + max-height:18mm; +} +.sticker .analysisrequest-info table td.sample-type { + width:100%; +} +.sticker .analysisrequest-info table, +.sticker .sampling-date-info table { + border-collapse:collapse; + font-size: 6pt; + width:100%; +} +.sticker .analysisrequest-info table td, +.sticker .sampling-date-info table td { + border:none; + padding-left:2mm; +} diff --git a/bika/lims/browser/templates/stickers/Code_93_2x38mm.pt b/bika/lims/browser/templates/stickers/Code_93_2x38mm.pt new file mode 100644 index 0000000000..00e1a1f8c4 --- /dev/null +++ b/bika/lims/browser/templates/stickers/Code_93_2x38mm.pt @@ -0,0 +1,77 @@ + + + + + + + + +
+ + + +
+
+ + +
+ + + + + +
+ + + +
+
+
+
+
diff --git a/bika/lims/browser/templates/stickers_preview.pt b/bika/lims/browser/templates/stickers_preview.pt index b6009e12fc..d0dbf6bf57 100644 --- a/bika/lims/browser/templates/stickers_preview.pt +++ b/bika/lims/browser/templates/stickers_preview.pt @@ -61,6 +61,27 @@ background-color: #bbb; text-shadow: 1px 1px 1px #aaa; } + #sticker-rule { + color: rgb(51, 51, 51); + height: 10mm; + margin: 0 auto; + overflow: hidden; + display:none; + } + .sticker-rule-tick, + .sticker-rule-tick-odd { + display:table-cell; + width:10mm; + min-width:10mm; + max-width:10mm; + font-size:6pt; + } + .sticker-rule-tick { + border-top:2px solid #666; + } + .sticker-rule-tick-odd { + border-top:1px dotted #666; + } .sticker { margin:0 auto; background-color:#fff; @@ -78,6 +99,9 @@ box-shadow:none; border-bottom:none; } + #sticker-rule { + display:none !important; + } }