diff --git a/bika/lims/__init__.py b/bika/lims/__init__.py index 17de1d0f9d..74ba288369 100644 --- a/bika/lims/__init__.py +++ b/bika/lims/__init__.py @@ -31,11 +31,16 @@ allow_module('bika.lims.permissions') allow_module('bika.lims.utils') allow_module('json') -allow_module('pdb') allow_module('zope.i18n.locales') allow_module('zope.component') allow_module('plone.registry.interfaces') +import App +debug_mode = App.config.getConfiguration().debug_mode +if debug_mode: + allow_module('pdb') + allow_module('pudb') + def initialize(context): from content.analysis import Analysis diff --git a/bika/lims/browser/analysisrequest/add.py b/bika/lims/browser/analysisrequest/add.py index c7e8be919a..8925b6910e 100644 --- a/bika/lims/browser/analysisrequest/add.py +++ b/bika/lims/browser/analysisrequest/add.py @@ -1,6 +1,5 @@ import json from bika.lims.utils.sample import create_sample -from bika.lims.utils.samplepartition import create_samplepartition from bika.lims.workflow import doActionFor import plone @@ -16,7 +15,7 @@ from bika.lims.utils import getHiddenAttributesForClass, dicts_to_dict from bika.lims.utils import t from bika.lims.utils import tmpID -from bika.lims.utils.analysisrequest import create_analysisrequest +from bika.lims.utils.analysisrequest import create_analysisrequest as crar from magnitude import mg from plone.app.layout.globals.interfaces import IViewView from Products.Archetypes import PloneMessageFactory as PMF @@ -443,7 +442,7 @@ def __call__(self): ARs = [] for arnum, state in valid_states.items(): # Create the Analysis Request - ar = create_analysisrequest( + ar = crar( portal_catalog(UID=state['Client'])[0].getObject(), self.request, state @@ -471,170 +470,9 @@ def __call__(self): return json.dumps({'success': message}) - +from bika.lims import deprecated +@deprecated(comment="bika.lims.browser.analysisrequest.add." + "create_analysisrequest is deprecated and will be removed " + "in Bika LIMS 3.3", replacement=crar) def create_analysisrequest(context, request, values): - """Create an AR. - - :param context the container in which the AR will be created (Client) - :param request the request object - :param values a dictionary containing fieldname/value pairs, which - will be applied. Some fields will have specific code to handle them, - and others will be directly written to the schema. - :return the new AR instance - - Special keys present (or required) in the values dict, which are not present - in the schema: - - - Partitions: data about partitions to be created, and the - analyses that are to be assigned to each. - - - Prices: custom prices set in the HTML form. - - - ResultsRange: Specification values entered in the HTML form. - - """ - # Gather neccesary tools - workflow = getToolByName(context, 'portal_workflow') - bc = getToolByName(context, 'bika_catalog') - - # Create new sample or locate the existing for secondary AR - sample = False - if values['Sample']: - if ISample.providedBy(values['Sample']): - secondary = True - sample = values['Sample'] - samplingworkflow_enabled = sample.getSamplingWorkflowEnabled() - else: - brains = bc(UID=values['Sample']) - if brains: - secondary = True - sample = brains[0].getObject() - samplingworkflow_enabled = sample.getSamplingWorkflowEnabled() - if not sample: - secondary = False - sample = create_sample(context, request, values) - samplingworkflow_enabled = context.bika_setup.getSamplingWorkflowEnabled() - - # Create the Analysis Request - ar = _createObjectByType('AnalysisRequest', context, tmpID()) - ar.setSample(sample) - - # processform renames the sample, this requires values to contain the Sample. - values['Sample'] = sample - ar.processForm(REQUEST=request, values=values) - - # Object has been renamed - ar.edit(RequestID=ar.getId()) - - # Set initial AR state - workflow_action = 'sampling_workflow' if samplingworkflow_enabled \ - else 'no_sampling_workflow' - workflow.doActionFor(ar, workflow_action) - - - # We need to send a list of service UIDS to setAnalyses function. - # But we may have received a list of titles, list of UIDS, - # list of keywords or list of service objects! - service_uids = [] - for obj in values['Analyses']: - uid = False - # service objects - if hasattr(obj, 'portal_type') and obj.portal_type == 'AnalysisService': - uid = obj.UID() - # Analysis objects (shortcut for eg copying analyses from other AR) - elif hasattr(obj, 'portal_type') and obj.portal_type == 'Analysis': - uid = obj.getService() - # Maybe already UIDs. - if not uid: - bsc = getToolByName(context, 'bika_setup_catalog') - brains = bsc(portal_type='AnalysisService', UID=obj) - if brains: - uid = brains[0].UID - # Maybe already UIDs. - if not uid: - bsc = getToolByName(context, 'bika_setup_catalog') - brains = bsc(portal_type='AnalysisService', title=obj) - if brains: - uid = brains[0].UID - if uid: - service_uids.append(uid) - else: - logger.info("In analysisrequest.add.create_analysisrequest: cannot " - "find uid of this service: %s" % obj) - - # Set analysis request analyses - ar.setAnalyses(service_uids, - prices=values.get("Prices", []), - specs=values.get('ResultsRange', [])) - analyses = ar.getAnalyses(full_objects=True) - - skip_receive = ['to_be_sampled', 'sample_due', 'sampled', 'to_be_preserved'] - if secondary: - # Only 'sample_due' and 'sample_recieved' samples can be selected - # for secondary analyses - doActionFor(ar, 'sampled') - doActionFor(ar, 'sample_due') - sample_state = workflow.getInfoFor(sample, 'review_state') - if sample_state not in skip_receive: - doActionFor(ar, 'receive') - - for analysis in analyses: - doActionFor(analysis, 'sample_due') - analysis_state = workflow.getInfoFor(analysis, 'review_state') - if analysis_state not in skip_receive: - doActionFor(analysis, 'receive') - - if not secondary: - # Create sample partitions - partitions = [] - for n, partition in enumerate(values['Partitions']): - # Calculate partition id - partition_prefix = sample.getId() + "-P" - partition_id = '%s%s' % (partition_prefix, n + 1) - partition['part_id'] = partition_id - # Point to or create sample partition - if partition_id in sample.objectIds(): - partition['object'] = sample[partition_id] - else: - partition['object'] = create_samplepartition( - sample, - partition - ) - # now assign analyses to this partition. - obj = partition['object'] - for analysis in analyses: - if analysis.getService().UID() in partition['services']: - analysis.setSamplePartition(obj) - - partitions.append(partition) - - # If Preservation is required for some partitions, - # and the SamplingWorkflow is disabled, we need - # to transition to to_be_preserved manually. - if not samplingworkflow_enabled: - to_be_preserved = [] - sample_due = [] - lowest_state = 'sample_due' - for p in sample.objectValues('SamplePartition'): - if p.getPreservation(): - lowest_state = 'to_be_preserved' - to_be_preserved.append(p) - else: - sample_due.append(p) - for p in to_be_preserved: - doActionFor(p, 'to_be_preserved') - for p in sample_due: - doActionFor(p, 'sample_due') - doActionFor(sample, lowest_state) - doActionFor(ar, lowest_state) - - # Transition pre-preserved partitions - for p in partitions: - if 'prepreserved' in p and p['prepreserved']: - part = p['object'] - state = workflow.getInfoFor(part, 'review_state') - if state == 'to_be_preserved': - workflow.doActionFor(part, 'preserve') - - # Return the newly created Analysis Request - return ar + return crar(context, request, values) diff --git a/bika/lims/browser/analysisrequest/templates/ar_add_by_col.pt b/bika/lims/browser/analysisrequest/templates/ar_add_by_col.pt index 78b1a9c472..d4dcf1a6db 100644 --- a/bika/lims/browser/analysisrequest/templates/ar_add_by_col.pt +++ b/bika/lims/browser/analysisrequest/templates/ar_add_by_col.pt @@ -140,22 +140,6 @@ - diff --git a/bika/lims/browser/js/bika.lims.loader.js b/bika/lims/browser/js/bika.lims.loader.js index 71b0be88f6..d26d108283 100644 --- a/bika/lims/browser/js/bika.lims.loader.js +++ b/bika/lims/browser/js/bika.lims.loader.js @@ -79,8 +79,8 @@ window.bika.lims.controllers = { ['ClientSamplingRoundAddEditView'], // Sampling Rounds PrintView - ".samplinground-print-form": - ['SamplingRoundtPrintView'], + "#sr_publish_container": + ['SamplingRoundPrintView'], // Reference Samples ".portaltype-referencesample.template-analyses": @@ -211,18 +211,11 @@ window.bika.lims.loadControllers = function(all, controllerKeys) { controllers[key].forEach(function(js) { if (all == true || $.inArray(key, controllerKeys) >= 0 || $.inArray(js, _bika_lims_loaded_js) < 0) { console.debug('[bika.lims.loader] Loading '+js); - try { - obj = new window[js](); - obj.load(); - // Register the object for further access - window.bika.lims[js]=obj; - _bika_lims_loaded_js.push(js); - } catch (e) { - // statements to handle any exceptions - var msg = '[bika.lims.loader] Unable to load '+js+": "+ e.message +"\n"+e.stack; - console.warn(msg); - window.bika.lims.error(msg); - } + obj = new window[js](); + obj.load(); + // Register the object for further access + window.bika.lims[js]=obj; + _bika_lims_loaded_js.push(js); } }); } diff --git a/bika/lims/browser/js/bika.lims.samplinground.print.js b/bika/lims/browser/js/bika.lims.samplinground.print.js index b0777b0fbe..87b26a635a 100644 --- a/bika/lims/browser/js/bika.lims.samplinground.print.js +++ b/bika/lims/browser/js/bika.lims.samplinground.print.js @@ -1,57 +1,81 @@ /** * Controller class for SamplingRound Print View */ -function SamplingRoundtPrintView() { +function SamplingRoundPrintView() { var that = this; - var referrer_cookie_name = '_rspv'; + 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("sr.print.urlback", backurl); + createCookie(cookiename, backurl); } else { - backurl = readCookie("sr.print.urlback"); + backurl = readCookie(cookiename); + // Fallback to portal_url instead of staying inside publish. if (!backurl) { backurl = portal_url; } } - load_barcodes(); + $('#sel_format').change(function(e) { + reloadReport(); + }); - $('#print_button').click(function(e) { - e.preventDefault(); - window.print(); + $('#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) { - e.preventDefault(); - location.href = backurl; + location.href=backurl; }); - - $('#template').change(function(e) { + $('#print_button').click(function(e) { + e.preventDefault(); var url = window.location.href; - var seltpl = $(this).val(); - var selcols = $("#numcols").val(); - $('#samplinground-printview').animate({opacity:0.2}, 'slow'); - $.ajax({ - url: url, - type: 'POST', - data: { "template":seltpl, - "numcols":selcols} - }) - .always(function(data) { - var htmldata = data; - var cssdata = $(htmldata).find('#report-style').html(); - $('#report-style').html(cssdata); - htmldata = $(htmldata).find('#samplinground-printview').html(); - $('#samplinground-printview').html(htmldata); - $('#samplinground-printview').animate({opacity:1}, 'slow'); - load_barcodes(); + $('#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)) @@ -72,5 +96,284 @@ function SamplingRoundtPrintView() { '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/samplinground/configure.zcml b/bika/lims/browser/samplinground/configure.zcml index 01fc7f04a7..53a13499cf 100644 --- a/bika/lims/browser/samplinground/configure.zcml +++ b/bika/lims/browser/samplinground/configure.zcml @@ -29,7 +29,7 @@ diff --git a/bika/lims/browser/samplinground/print.py b/bika/lims/browser/samplinground/printform.py similarity index 76% rename from bika/lims/browser/samplinground/print.py rename to bika/lims/browser/samplinground/printform.py index 097e92d962..63fd332866 100644 --- a/bika/lims/browser/samplinground/print.py +++ b/bika/lims/browser/samplinground/printform.py @@ -1,11 +1,15 @@ 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): @@ -21,7 +25,7 @@ def __call__(self): self._samplingrounds = [self.context] elif self.context.portal_type == 'SamplingRounds' \ - and self.request.get('items', ''): + 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)] @@ -33,9 +37,13 @@ def __call__(self): self.destination_url = self.request.get_header("referer", self.context.absolute_url()) - # Generate PDF? + # Do print? if self.request.form.get('pdf', '0') == '1': - return self._flush_pdf() + 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() @@ -47,7 +55,7 @@ def getSamplingRoundObj(self): def getSRTemplates(self): """ Returns a DisplayList with the available templates found in - browser/samplingrpund/templates/ + browser/samplinground/templates/ """ this_dir = os.path.dirname(os.path.abspath(__file__)) templates_dir = os.path.join(this_dir, self._TEMPLATES_DIR) @@ -90,17 +98,29 @@ def getFormTemplate(self): if self._current_sr_index < len(self._samplingrounds): self._current_sr_index += 1 return reptemplate - # Using default for now - return ViewPageTemplateFile("templates/print/default_form.pt")(self) 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 """ - 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, 'default') - content_file = open(path, 'r') - return content_file.read() + 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): """ @@ -178,7 +198,7 @@ def getAnalysisRequestBySample(self): 'sampling_point': { 'hidden': True if arcell else False, 'rowspan': numans, - 'value': ar.getSamplePoint().title, + 'value': ar.getSamplePoint().title if ar.getSamplePoint() else '', }, 'sampling_date': { 'hidden': True if arcell else False, @@ -218,3 +238,29 @@ def getLab(self): 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.css b/bika/lims/browser/samplinground/templates/print/default_form.css similarity index 93% rename from bika/lims/browser/samplinground/templates/print/default.css rename to bika/lims/browser/samplinground/templates/print/default_form.css index 50a57cfe92..96c36c588c 100644 --- a/bika/lims/browser/samplinground/templates/print/default.css +++ b/bika/lims/browser/samplinground/templates/print/default_form.css @@ -2,14 +2,14 @@ .barcode-container { width:100%; } -div.sr_form_body { +div.report_body { font-size:0.8em; } -div.sr_form_body a { +div.report_body a { color:#000; text-decoration:none; } -div.sr_form_body h1 { +div.report_body h1 { padding-top:15px; font-size:1.7em; } diff --git a/bika/lims/browser/samplinground/templates/print_form.pt b/bika/lims/browser/samplinground/templates/print_form.pt index 9221e588f9..833f30adb3 100644 --- a/bika/lims/browser/samplinground/templates/print_form.pt +++ b/bika/lims/browser/samplinground/templates/print_form.pt @@ -14,132 +14,196 @@ portal portal_state/portal;">
- - - -
-
-
- - -
-
-    - + #sr_publish_header #sr_publish_buttons #print_button { + background-color:#0B486B; + } + #sr_publish_header #options_handler div.options-line { + padding:0 0 10px 0; + } + #sr_publish_header #options_handler input.option-margin { + border: 1px solid #bbb; + padding: 1px 2px; + width: 16px; + } + #sel_format_info { + padding: 0 15px 0 5px; + outline:0; + } + #sel_format_info img { + vertical-align:middle; + } + #sel_format_info_pane { + background-color: #EFEFEF; + border-top: 1px solid #008000; + line-height: 1.5em; + margin: 10px -20px 10px -10px; + padding: 10px 20px; + } + .page-break { + background-color: #cdcdcd; + height: 20px; + margin: 0mm -30mm; + } + .clearfix { + clear:both !important; + margin:0 !important; + padding:0 !important; + height:0 !important; + } + .page-footer.footer-invalid, + .page-header.header-invalid { + border: 1px dotted red; + color: red; + padding: 5px; + } + @media print { + a { + text-decoration:none; + color:#000; + } + div.sr_publish_page { + border: none; + } + html { + background-color:#fff; + margin:0 !important; + padding:0 !important; + } + body { + padding:0 !important; + margin:0 !important; + } + .page-break, .page-break-after, .page-break-before{ + display: block !important; + border:none !important; + padding:0 !important; + margin:0 !important; + background-color:transparent !important; + } + div.page-break { + page-break-after: always; + } + div.page-break-after { + page-break-after: always; + } + div.page-break-before { + page-break-before: always; + } + .page-footer { + margin:0 !important; + border:none !important; + background-color:#ffffff; + } + #sr_publish_header { + display:none; + visibility:hidden; + } + .page-footer.footer-invalid, + .page-header.header-invalid { + display:none; + } + } + + + +
+
-
- - - -
-
-
-
+ +
diff --git a/bika/lims/browser/templates/bika_listing_table_items.pt b/bika/lims/browser/templates/bika_listing_table_items.pt index 6f033f78bd..cfc99e48bb 100644 --- a/bika/lims/browser/templates/bika_listing_table_items.pt +++ b/bika/lims/browser/templates/bika_listing_table_items.pt @@ -394,9 +394,9 @@ Table cells for each column from in review_state's column list. - Range remarks:  + Range remarks:  @@ -440,9 +439,9 @@ Table cells for each column from in review_state's column list. - Remarks:  + Remarks:  diff --git a/bika/lims/configure.zcml b/bika/lims/configure.zcml index aeab2c481d..0f634288db 100644 --- a/bika/lims/configure.zcml +++ b/bika/lims/configure.zcml @@ -10,6 +10,7 @@ + // // @@ -19,6 +20,8 @@ + + diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index dad27f9e95..6f1a0a9921 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -1547,6 +1547,15 @@ def getAnalysisCategory(self): value.append(val) return value + def getAnalysisCategoryIDs(self): + proxies = self.getAnalyses(full_objects=True) + value = [] + for proxy in proxies: + val = proxy.getService().getCategory().id + if val not in value: + value.append(val) + return value + def getAnalysisService(self): proxies = self.getAnalyses(full_objects=True) value = [] diff --git a/bika/lims/content/samplinground.py b/bika/lims/content/samplinground.py index 194870cfb5..8aabdce402 100644 --- a/bika/lims/content/samplinground.py +++ b/bika/lims/content/samplinground.py @@ -105,6 +105,28 @@ def __call__(self, context): return SimpleVocabulary(terms) +class ClientContacts(object): + """Context source binder to provide a vocabulary of the client contacts. + """ + implements(IContextSourceBinder) + + def __call__(self, context): + container = context.aq_parent + terms = [] + # Show only the client's + if container.portal_type == 'Client': + contacts = container.getContacts() + for cnt in contacts: + c_id = cnt.getId() + name = cnt.getFullname() + if not name: + name = c_id + terms.append( + SimpleVocabulary.createTerm( + c_id, str(c_id), name)) + return SimpleVocabulary(terms) + + class ISamplingRound(model.Schema): """A Sampling round interface """ @@ -174,6 +196,18 @@ class ISamplingRound(model.Schema): ) ) + client_contact = schema.Choice( + title=_(u'Client contact who coordinates with the lab'), + required=False, + source=ClientContacts() + ) + + client_contact_in_charge_at_sampling_time = schema.Choice( + title=_(u'Client contact in charge at sampling time'), + required=False, + source=ClientContacts() + ) + num_sample_points = schema.Int( title=_(u"Number of Sample Points"), description=_(u"the total number of Sample Points defined in the Round."), @@ -237,7 +271,7 @@ def num_containers(self): return len(containers) def getAnalysisRequests(self): - """ Return all the Analysis Requests linked to the Sampling Round + """ Return all the Analysis Request brains linked to the Sampling Round """ # I have to get the catalog in this way because I can't do it with 'self'... pc = getToolByName(api.portal.get(), 'portal_catalog') @@ -310,6 +344,53 @@ def getSRTemplateInfo(self): logger.exception(error, self.sr_template) return srtdict + def getClientContact(self): + """ + Returns info from the Client contact who coordinates with the lab + """ + pc = getToolByName(api.portal.get(), 'portal_catalog') + contentFilter = {'portal_type': 'Contact', + 'id': self.client_contact} + cnt = pc(contentFilter) + cntdict = {'uid': '', 'id': '', 'fullname': '', 'url': ''} + if len(cnt) == 1: + cnt = cnt[0].getObject() + cntdict = { + 'uid': cnt.id, + 'id': cnt.UID(), + 'fullname': cnt.getFullname(), + 'url': cnt.absolute_url(), + } + else: + from bika.lims import logger + error = "Error when looking for contact with id '%s'. " + logger.exception(error, self.client_contact) + return cntdict + + def getClientInChargeAtSamplingTime(self): + """ + Returns info from the Client contact who is in charge at sampling time + """ + pc = getToolByName(api.portal.get(), 'portal_catalog') + contentFilter = {'portal_type': 'Contact', + 'id': self.client_contact_in_charge_at_sampling_time} + cnt = pc(contentFilter) + cntdict = {'uid': '', 'id': '', 'fullname': '', 'url': ''} + if len(cnt) == 1: + cnt = cnt[0].getObject() + cntdict = { + 'uid': cnt.id, + 'id': cnt.UID(), + 'fullname': cnt.getFullname(), + 'url': cnt.absolute_url(), + } + else: + from bika.lims import logger + error = "Error when looking for contact with id '%s'. " + logger.exception( + error, self.client_contact_in_charge_at_sampling_time) + return cntdict + def hasUserAddEditPermission(self): """ Checks if the current user has privileges to access to the editing view. diff --git a/bika/lims/skins/bika/attachments.pt b/bika/lims/skins/bika/attachments.pt index a90f67edc7..66ec4267aa 100644 --- a/bika/lims/skins/bika/attachments.pt +++ b/bika/lims/skins/bika/attachments.pt @@ -157,15 +157,15 @@ analyses python:[a for a in analyses if a.getService().getAttachmentOption() in ['p', 'r'] and a.getReviewState() not in ['retracted']]; sort_on python:(('id', 'nocase', 'asc'),); analyses python:sequence.sort(analyses, sort_on); - a python:[a for a in analyses if a.portal_type == 'Analysis']; + a_analyses python:[a for a in analyses if a.portal_type == 'Analysis']; bc python:[a for a in analyses if a.portal_type == 'ReferenceAnalysis']; - b python:[a for a in bc if a.aq_parent.getBlank()]; - c python:[a for a in bc if not a.aq_parent.getBlank()]; - d python:[a for a in analyses if a.portal_type == 'DuplicateAnalysis'];"> + b_analyses python:[a for a in bc if a.aq_parent.getBlank()]; + c_analyses python:[a for a in bc if not a.aq_parent.getBlank()]; + d_analyses python:[a for a in analyses if a.portal_type == 'DuplicateAnalysis'];"> Attachment Type -      +   - + - + - + - +