diff --git a/src/bika/coa/__init__.py b/src/bika/coa/__init__.py index 05301bc..d585fa5 100644 --- a/src/bika/coa/__init__.py +++ b/src/bika/coa/__init__.py @@ -5,13 +5,23 @@ # Copyright 2019 by it's authors. import logging +from bika.lims.api import get_request +from bika.coa.interfaces import IBikaCOALayer +from zope.i18nmessageid import MessageFactory PRODUCT_NAME = "bika.coa" PROFILE_ID = "profile-{}:default".format(PRODUCT_NAME) logger = logging.getLogger(PRODUCT_NAME) +_ = MessageFactory(PRODUCT_NAME) def initialize(context): """Initializer called when used as a Zope 2 product.""" logger.info("*** Initializing BIKA.COA ***") + + +def is_installed(): + """Returns whether the product is installed or not""" + request = get_request() + return IBikaCOALayer.providedBy(request) diff --git a/src/bika/coa/ajax.py b/src/bika/coa/ajax.py index 5dc3268..1b3f0e5 100644 --- a/src/bika/coa/ajax.py +++ b/src/bika/coa/ajax.py @@ -72,17 +72,22 @@ def ajax_save_reports(self): sample = getAdapter(sample, ISuperModel) samples.append(sample) - + csv_reports = [] is_multi_template = self.is_multi_template(template) - if template == "bika.coa:MultiGeochemistryBatch.pt": + if template == "bika.coa:GeoangolBatchMulti.pt": csv_report = self.create_geochemistry_csv_report(samples,coa_num) csv_reports = [csv_report for i in range(len(pdf_reports))] - elif template == "bika.coa:MultiBatch.pt": + elif template == "bika.coa:AWTCBatchMulti.pt": csv_report= self.create_batch_csv_reports(samples,coa_num) csv_reports = [csv_report for i in range(len(pdf_reports))] - if template == "bika.coa:MultiSampleTransposed.pt": + elif template == "bika.coa:ZimlabsTransposedMulti.pt": csv_report = self.create_zlabs_csv_report(samples,coa_num) csv_reports = [csv_report for i in range(len(pdf_reports))] + elif template == 'bika.hydrochem:HydroChemSystemsMulti.pt': + csv_reports = [""] + elif template == "bika.cannabis:CannabisUSMulti.pt": + csv_report = self.create_csv_reports_cannabis(samples, coa_num) + csv_reports = [csv_report for i in range(len(pdf_reports))] elif is_multi_template: csv_report = self.create_csv_reports(samples) csv_reports = [csv_report for i in range(len(pdf_reports))] @@ -199,12 +204,19 @@ def create_batch_csv_reports(self,samples,coa_num): if len(headers_line) != len(top_headers): for i in top_headers[len(headers_line):]: headers_line.append('') - - sample_data, penultimate_field_analyses, penultimate_lab_analyses = self.create_sample_rows(group_cats,field_analyses,lab_analyses) - header_rows = self.merge_header_and_values(top_headers,headers_line) - final_field_analyses = self.sort_analyses_to_list(penultimate_field_analyses) - final_lab_analyses = self.sort_analyses_to_list(penultimate_lab_analyses) + sample_data = self.get_sample_header_data(samples) + sample_ids = sample_data[0] + penultimate_field_analyses = self.create_field_sample_rows( + group_cats, field_analyses, sample_ids) + penultimate_lab_analyses = self.create_lab_sample_rows( + group_cats, lab_analyses, sample_ids) + + header_rows = self.merge_header_and_values(top_headers, headers_line) + final_field_analyses = self.sort_analyses_to_list( + penultimate_field_analyses) + final_lab_analyses = self.sort_analyses_to_list( + penultimate_lab_analyses) for row in header_rows: writer.writerow(row) @@ -405,31 +417,40 @@ def sort_analyses_to_list(self,analyses): key_sort = sorted(title_sort, key=lambda x:(x[1][-1] is None,x[1][-1])) return key_sort - def create_sample_rows(self,grouped_analyses,field_analyses,lab_analyses): - sample_ids = ["Sample ID"] - sample_Points = ["Sample Points"] - sample_Types = ["Sample Types"] + def create_field_sample_rows( + self, grouped_analyses, field_analyses, sample_ids): + if grouped_analyses.get("field"): - for field_Analysis_service in grouped_analyses.get("field"): - if field_Analysis_service.get("sampleID") not in sample_ids: - sample_ids.append(field_Analysis_service.get("sampleID")) - sample_Points.append(field_Analysis_service.get("samplePoint")) - sample_Types.append(field_Analysis_service.get("sampleType")) - position_at_top = sample_ids.index(field_Analysis_service.get("sampleID")) - 1 - title = field_Analysis_service.get('title') - field_analyses[title][position_at_top] = field_Analysis_service.get("result") + for field_AS in grouped_analyses.get("field"): + position_at_top = sample_ids.index( + field_AS.get("sampleID")) - 1 + title = field_AS.get('title') + field_analyses[title][position_at_top] = field_AS.get( + "result") + return field_analyses + + def create_lab_sample_rows( + self, grouped_analyses, lab_analyses, sample_ids): + if grouped_analyses.get("lab"): - for lab_Analysis_service in grouped_analyses.get("lab"): - if lab_Analysis_service.get("sampleID") not in sample_ids: - sample_ids.append(lab_Analysis_service.get("sampleID")) - sample_Points.append(lab_Analysis_service.get("samplePoint")) - sample_Types.append(lab_Analysis_service.get("sampleType")) - position_at_top = sample_ids.index(lab_Analysis_service.get("sampleID")) - 1 - title = lab_Analysis_service.get('title') + for lab_AS in grouped_analyses.get("lab"): + position_at_top = sample_ids.index( + lab_AS.get("sampleID")) - 1 + title = lab_AS.get('title') if lab_analyses.get(title): - lab_analyses.get(title)[position_at_top] = lab_Analysis_service.get("result") - sample_headers = [sample_ids,sample_Points,sample_Types] - return sample_headers,field_analyses,lab_analyses + lab_analyses.get(title)[position_at_top] = lab_AS.get( + "result") + return lab_analyses + + def get_sample_header_data(self, samples): + sample_ids = ["Sample ID"] + sample_points = ["Sample Points"] + sample_types = ["Sample Types"] + for sample in samples: + sample_ids.append(sample.Title()) + sample_points.append(sample.getSamplePointTitle()) + sample_types.append(sample.getSampleTypeTitle()) + return [sample_ids, sample_points, sample_types] def merge_header_and_values(self,headers,values): """Merge the headers and their values to make writing to CSV easier""" @@ -611,6 +632,94 @@ def create_csv_report(self, sample): return output.getvalue() + def create_csv_reports_cannabis(self, samples, coa_num): + analyses = [] + output = StringIO.StringIO() + client_headers = [["COA ID", coa_num], + ["Client", samples[0].Client.title], + ["Client Contact", samples[0].Contact.title]] + headers = ["Sample ID", "Tracking ID", "Client Sample ID", + "Batch ID", "License", "Sample Type", "Strain", + "Lot", "Cultivation Batch ID", "Quantity", + "Date Sampled", "Date Received", + "Date Verified", "Date Published"] + body = [] + for sample in samples: + date_received = sample.DateReceived + date_sampled = sample.DateSampled + if date_sampled: + date_sampled = date_sampled.strftime('%m-%d-%y') + if date_received: + date_received = date_received.strftime('%m-%d-%y') + date_verified = sample.getDateVerified() + if date_verified: + date_verified = date_verified.strftime('%m-%d-%y') + license_num = sample.License + if license_num: + license_num = license_num.license_number + strain = sample.Strain + if strain: + strain = strain.title + date_published = sample.DatePublished + if date_published: + date_published = date_published.strftime('%m-%d-%y') + + writer = csv.writer(output) + line = [ + sample.id, + sample.TrackingID, + sample.ClientSampleID, + sample.getBatchID(), + license_num, + sample.SampleTypeTitle, + strain, + sample.LotNumber, + sample.CultivationBatchID, + sample.Quantity, + date_sampled, + date_received, + date_verified, + date_published + ] + + analyses = sample.getAnalyses(full_objects=True) + group_cats = {} + for analysis in analyses: + analysis_info = {'title': analysis.Title(), + 'result': analysis.getFormattedResult(html=False), + 'unit': analysis.getService().getUnit()} + if analysis.getCategoryTitle() not in group_cats.keys(): + group_cats[analysis.getCategoryTitle()] = [] + group_cats[analysis.getCategoryTitle()].append(analysis_info) + + for g_cat in sorted(group_cats.keys()): + for a_info in group_cats[g_cat]: + title = a_info['title'] + result = a_info['result'] + unit = a_info['unit'] + title_unit = title + if unit: + title_unit = '{} ({})'.format(title, unit) + if len(line) != len(headers): + for i in headers[len(line):]: + line.append('') + + if title_unit not in headers: + headers.append(title_unit) + line.append(result) + else: + line[headers.index(title_unit)] = result + + body.append(line) + + for header in client_headers: + writer.writerow(header) + writer.writerow(headers) + for rec in body: + writer.writerow(rec) + + return output.getvalue() + def create_csv_reports(self, samples): analyses = [] output = StringIO.StringIO() diff --git a/src/bika/coa/browser/analysisrequest/published_results.py b/src/bika/coa/browser/analysisrequest/published_results.py deleted file mode 100644 index 38a55d0..0000000 --- a/src/bika/coa/browser/analysisrequest/published_results.py +++ /dev/null @@ -1,105 +0,0 @@ -from bika.lims import api -from bika.lims.browser.analysisrequest.published_results import \ - AnalysisRequestPublishedResults as ARPR -from bika.lims import bikaMessageFactory as _ -from bika.lims.browser.bika_listing import BikaListingView -from ZODB.POSException import POSKeyError - - -class AnalysisRequestPublishedResults(ARPR): - - def __init__(self, context, request): - BikaListingView.__init__(self, context, request) - self.context = context - self.request = request - - self.catalog = "portal_catalog" - self.contentFilter = { - 'portal_type': 'ARReport', - 'path': { - 'query': api.get_path(self.context), - 'depth': 1, - }, - 'sort_order': 'reverse' - } - self.context_actions = {} - self.show_select_column = True - self.show_workflow_action_buttons = False - self.form_id = 'published_results' - self.icon = "{}//++resource++bika.lims.images/report_big.png".format( - self.portal_url) - self.title = self.context.translate(_("Published results")) - self.columns = { - 'COANumber': {'title': _('COA')}, - 'Date': {'title': _('Published Date')}, - 'PublishedBy': {'title': _('Published By')}, - 'DownloadPDF': {'title': _('Download PDF')}, - 'DownloadCSV': {'title': _('Download CSV')}, - 'Recipients': {'title': _('Recipients')}, - } - self.review_states = [ - {'id': 'default', - 'title': 'All', - 'contentFilter': {}, - 'columns': [ - 'COANumber', - 'Date', - 'PublishedBy', - 'Recipients', - 'DownloadPDF', - 'DownloadCSV', - ] - }, - ] - - def folderitem(self, obj, item, index): - - # TODO: find out why obj is a brain and not an object. - # see senaite.listing, obj is a brain and not an object - if api.is_brain: - obj = obj.getObject() - - item['PublishedBy'] = self.user_fullname(obj.Creator()) - - # Formatted creation date of report - creation_date = obj.created() - fmt_date = self.ulocalized_time(creation_date, long_format=1) - item['Date'] = fmt_date - - # Recipients as mailto: links - recipients = obj.getRecipients() - links = ["{Fullname}".format(**r) - for r in recipients if r['EmailAddress']] - if len(links) == 0: - links = ["{Fullname}".format(**r) for r in recipients] - item['replace']['Recipients'] = ', '.join(links) - - # download link 'Download PDF (size)' - dll = [] - try: # - pdf_data = obj.getPdf() - assert pdf_data - item['COANumber'] = '' - item['after']['COANumber'] = pdf_data.filename.split('.')[0] - z = pdf_data.get_size() - z = z / 1024 if z > 0 else 0 - dll.append("{}".format( - obj.absolute_url(), _("Download PDF"), z)) - except (POSKeyError, AssertionError): - # POSKeyError: 'No blob file' - pass - item['DownloadPDF'] = '' - item['after']['DownloadPDF'] = ', '.join(dll) - # download link 'Download CSV (size)' - dll = [] - if hasattr(obj, 'CSV'): - try: - dll.append("{}".format( - obj.absolute_url(), _("Download CSV"), 0)) - except (POSKeyError, AssertionError): - # POSKeyError: 'No blob file' - pass - item['DownloadCSV'] = '' - item['after']['DownloadCSV'] = ', '.join(dll) - - return item diff --git a/src/bika/coa/browser/configure.zcml b/src/bika/coa/browser/configure.zcml new file mode 100644 index 0000000..68838ac --- /dev/null +++ b/src/bika/coa/browser/configure.zcml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/bika/coa/browser/listingview/__init__.py b/src/bika/coa/browser/listingview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bika/coa/browser/listingview/configure.zcml b/src/bika/coa/browser/listingview/configure.zcml new file mode 100644 index 0000000..664e3d2 --- /dev/null +++ b/src/bika/coa/browser/listingview/configure.zcml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/bika/coa/browser/listingview/published_results.py b/src/bika/coa/browser/listingview/published_results.py new file mode 100644 index 0000000..8dacc22 --- /dev/null +++ b/src/bika/coa/browser/listingview/published_results.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from zope.component import adapts +from zope.interface import implements + +from bika.coa import is_installed +from bika.coa.browser.listingview.report_listing import \ + ReportsListingViewAdapter as RLVA +from bika.lims import api +from senaite.app.listing.interfaces import IListingView +from senaite.app.listing.interfaces import IListingViewAdapter + + +class AnalysisRequestPublishedResults(RLVA): + adapts(IListingView) + implements(IListingViewAdapter) + + def before_render(self): + if not is_installed(): + return + # get the client for the catalog query + client = self.context.getClient() + client_path = api.get_path(client) + + # get the UID of the current context (sample) + sample_uid = api.get_uid(self.context) + + self.contentFilter = { + "portal_type": "ARReport", + "path": { + "query": client_path, + "depth": 2, + }, + # search all reports, where the current sample UID is included + "sample_uid": [sample_uid], + "sort_on": "created", + "sort_order": "descending", + } diff --git a/src/bika/coa/browser/listingview/report_listing.py b/src/bika/coa/browser/listingview/report_listing.py new file mode 100644 index 0000000..1f93491 --- /dev/null +++ b/src/bika/coa/browser/listingview/report_listing.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +import collections +from ZODB.POSException import POSKeyError +from zope.component import adapts +from zope.interface import implements + +from bika.lims import api +from bika.lims.utils import get_link +from bika.coa import is_installed +from bika.coa import _ +from senaite.app.listing.interfaces import IListingView +from senaite.app.listing.interfaces import IListingViewAdapter + + +class ReportsListingViewAdapter(object): + adapts(IListingView) + implements(IListingViewAdapter) + + def __init__(self, listing, context): + self.listing = listing + self.context = context + + def before_render(self): + if not is_installed(): + return + self.listing.columns = collections.OrderedDict(( + ("Info", { + "title": "", + "toggle": True},), + ("COA", { + "title": _("COA"), + "index": "sortable_title"},), + ("Batch", { + "title": _("Batch")},), + ("State", { + "title": _("Review State")},), + ("PDF", { + "title": _("Download PDF")},), + ("FileSize", { + "title": _("Filesize")},), + ("CSV", { + "title": _("Download CSV")},), + ("Date", { + "title": _("Published Date")},), + ("PublishedBy", { + "title": _("Published By")},), + ("Sent", { + "title": _("Email sent")},), + ("Recipients", { + "title": _("Recipients")},), + )) + for i in range(len(self.listing.review_states)): + self.listing.review_states[i]["columns"].append("COA") + self.listing.review_states[i]["columns"].append("CSV") + + def folder_item(self, obj, item, index): + if not is_installed(): + return item + obj = api.get_object(obj) + csv = self.get_csv(obj) + filesize_csv = self.listing.get_filesize(csv) + if filesize_csv > 0: + url = "{}/at_download/CSV".format(obj.absolute_url()) + item["replace"]["CSV"] = get_link( + url, value="CSV", target="_blank") + + pdf = self.listing.get_pdf(obj) + filesize_pdf = self.listing.get_filesize(pdf) + if filesize_pdf > 0: + url = "{}/at_download/Pdf".format(obj.absolute_url()) + item["replace"]["PDF"] = get_link( + url, value="PDF", target="_blank") + + pdf_filename = pdf.filename + pdf_filename_value = 'Unknown' + if pdf_filename: + pdf_filename_value = pdf_filename.split('.')[0] + ar = obj.getAnalysisRequest() + item["replace"]["COA"] = get_link( + ar.absolute_url(), value=pdf_filename_value + ) + + return item + + def get_csv(self, obj): + """Get the report csv + """ + try: + return obj.CSV + except (POSKeyError, TypeError): + return None diff --git a/src/bika/coa/browser/overrides.zcml b/src/bika/coa/browser/overrides.zcml index 8cf164a..3ae1fe7 100644 --- a/src/bika/coa/browser/overrides.zcml +++ b/src/bika/coa/browser/overrides.zcml @@ -3,22 +3,6 @@ xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="bika.coa"> - - - - @@ -56,5 +40,9 @@ layer="bika.lims.interfaces.IBikaLIMS" /> + + diff --git a/src/bika/coa/browser/publish/emailview.py b/src/bika/coa/browser/publish/emailview.py index 6c332b3..582d05d 100644 --- a/src/bika/coa/browser/publish/emailview.py +++ b/src/bika/coa/browser/publish/emailview.py @@ -45,6 +45,37 @@ def email_csv_report_enabled(self): logger.info("email_csv_report_enabled: is {}".format(enabled)) return enabled + @property + def get_batch(self): + reports = self.reports + batch_id = None + for num, report in enumerate(reports): + samples = report.getContainedAnalysisRequests() + if all([getattr(i.getBatch(), "id", '') for i in samples]): + batch_id = samples[0].getBatch().id + break + return batch_id + + @property + def email_body(self): + """Email body text to be used in the template + """ + # request parameter has precedence + body = self.request.get("body", None) + if body is not None: + return body + setup = api.get_setup() + body = setup.getEmailBodySamplePublication() + template_context = { + "client_name": self.client_name, + "lab_name": self.lab_name, + "lab_address": self.lab_address, + "batch_id": self.get_batch, + } + rendered_body = self.render_email_template( + body, template_context=template_context) + return rendered_body + @property def email_attachments(self): logger.info("email_attachments bika.coa: entered") @@ -90,19 +121,25 @@ def email_attachments(self): def get_report_data(self, report): """Report data to be used in the template """ - sample = report.getAnalysisRequest() - # sample attachments only - attachments = sample.getAttachment() - attachments_data = map(self.get_attachment_data, attachments) + primary_sample = report.getAnalysisRequest() + samples = report.getContainedAnalysisRequests() or [primary_sample] + attachments_data = [] + + for sample in samples: + for attachment in self.get_all_sample_attachments(sample): + attachment_data = self.get_attachment_data(sample, attachment) + attachments_data.append(attachment_data) + pdf = self.get_pdf(report) filesize = "{} Kb".format(self.get_filesize(pdf)) + filename = self.get_report_filename(report) return { - "sample": sample, + "sample": primary_sample, "attachments": attachments_data, "pdf": pdf, "obj": report, "uid": api.get_uid(report), "filesize": filesize, - "filename": pdf.filename, + "filename": filename, } diff --git a/src/bika/coa/browser/publish/reports_listing.py b/src/bika/coa/browser/publish/reports_listing.py deleted file mode 100644 index 2a88ca5..0000000 --- a/src/bika/coa/browser/publish/reports_listing.py +++ /dev/null @@ -1,274 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of SENAITE.CORE. -# -# SENAITE.CORE is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, version 2. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 51 -# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Copyright 2018-2020 by it's authors. -# Some rights reserved, see README and LICENSE. - -import collections - -from bika.lims import api -from bika.lims import bikaMessageFactory as _BMF -from bika.lims import senaiteMessageFactory as _ -from bika.lims.browser.publish.reports_listing import ReportsListingView as RLV -from bika.lims.utils import get_link -from bika.lims.utils import to_utf8 -from Products.CMFPlone.utils import safe_unicode -from ZODB.POSException import POSKeyError - - -class ReportsListingView(RLV): - """Listing view of all generated reports - """ - - def __init__(self, context, request): - super(ReportsListingView, self).__init__(context, request) - - self.catalog = "portal_catalog" - self.contentFilter = { - "portal_type": "ARReport", - "path": { - "query": api.get_path(self.context), - "depth": 2, - }, - "sort_on": "created", - "sort_order": "descending", - } - - self.form_id = "reports_listing" - self.title = _("Analysis Reports") - - self.icon = "{}/{}".format( - self.portal_url, - "++resource++bika.lims.images/report_big.png" - ) - self.context_actions = {} - - self.allow_edit = False - self.show_select_column = True - self.show_workflow_action_buttons = True - self.pagesize = 30 - - help_email_text = _( - "Open email form to send the selected reports to the recipients. " - "This will also publish the contained samples of the reports " - "after the email was successfully sent.") - - self.send_email_transition = { - "id": "send_email", - "title": _("Email"), - "url": "email", - "css_class": "btn-primary", - "help": help_email_text, - } - - help_publish_text = _( - "Manually publish all contained samples of the selected reports.") - - self.publish_samples_transition = { - "id": "publish_samples", - "title": _("Publish"), - # see senaite.core.browser.workflow - "url": "workflow_action?action=publish_samples", - "css_class": "btn-success", - "help": help_publish_text, - } - - self.columns = collections.OrderedDict(( - ("Info", { - "title": "", - "toggle": True},), - ("COA", { - "title": _("COA"), - "index": "sortable_title"},), - ("State", { - "title": _("Review State")},), - ("PDF", { - "title": _("Download PDF")},), - ("CSV", { - "title": _("Download CSV")},), - ("FileSize", { - "title": _("Filesize")},), - ("Date", { - "title": _("Published Date")},), - ("PublishedBy", { - "title": _("Published By")},), - ("Recipients", { - "title": _("Recipients")},), - )) - - self.review_states = [ - { - "id": "default", - "title": "All", - "contentFilter": {}, - "columns": self.columns.keys(), - "custom_transitions": [ - self.send_email_transition, - self.publish_samples_transition, - ] - }, - ] - - def get_filesize(self, pdf): - """Compute the filesize of the PDF - """ - try: - filesize = float(pdf.get_size()) - return filesize / 1024 - except (POSKeyError, TypeError): - return 0 - - def localize_date(self, date): - """Return the localized date - """ - return self.ulocalized_time(date, long_format=1) - - def get_pdf(self, obj): - """Get the report PDF - """ - try: - return obj.getPdf() - except (POSKeyError, TypeError): - return None - - def get_csv(self, obj): - """Get the report csv - """ - try: - return obj.CSV - except (POSKeyError, TypeError): - return None - - def folderitem(self, obj, item, index): - """Augment folder listing item - """ - - # Quick fix, object here seems to have change from being an ar to being - # a report, will investigate further, but chainging the object using - # getObject seems to fix the current error - # check on senaite.core./browser/publish/emailview.py for clues - obj = obj.getObject() - ar = obj.getAnalysisRequest() - uid = api.get_uid(obj) - review_state = api.get_workflow_status_of(ar) - status_title = review_state.capitalize().replace("_", " ") - - # Report Info Popup - # see: bika.lims.site.coffee for the attached event handler - item["Info"] = get_link( - "analysisreport_info?report_uid={}".format(uid), - value="", - css_class="service_info") - - pdf = self.get_pdf(obj) - pdf_filename = pdf.filename - pdf_filename_value = 'Unknown' - if pdf_filename: - pdf_filename_value = pdf_filename.split('.')[0] - item["replace"]["COA"] = get_link( - ar.absolute_url(), value=pdf_filename_value - ) - - filesize = self.get_filesize(pdf) - if filesize > 0: - url = "{}/at_download/Pdf".format(obj.absolute_url()) - item["replace"]["PDF"] = get_link( - url, value="PDF", target="_blank") - - csv = self.get_csv(obj) - filesize_csv = self.get_filesize(csv) - if filesize_csv > 0: - url = "{}/at_download/CSV".format(obj.absolute_url()) - item["replace"]["CSV"] = get_link( - url, value="CSV", target="_blank") - - item["State"] = _BMF(status_title) - item["state_class"] = "state-{}".format(review_state) - item["FileSize"] = "{:.2f} Kb".format(filesize) - fmt_date = self.localize_date(obj.created()) - item["Date"] = fmt_date - item["PublishedBy"] = self.user_fullname(obj.Creator()) - - # N.B. There is a bug in the current publication machinery, so that - # only the primary contact get stored in the Attachment as recipient. - # - # However, we're interested to show here the full list of recipients, - # so we use the recipients of the containing AR instead. - recipients = [] - - for recipient in self.get_recipients(ar): - email = safe_unicode(recipient["EmailAddress"]) - fullname = safe_unicode(recipient["Fullname"]) - if email: - value = u"{}".format(email, fullname) - recipients.append(value) - else: - message = _("No email address set for this contact") - value = u"" \ - u"⚠ {}".format(message, fullname) - recipients.append(value) - - item["replace"]["Recipients"] = ", ".join(recipients) - - # No recipient with email set preference found in the AR, so we also - # flush the Recipients data from the Attachment - if not recipients: - item["Recipients"] = "" - - return item - - def get_recipients(self, ar): - """Return the AR recipients in the same format like the AR Report - expects in the records field `Recipients` - """ - plone_utils = api.get_tool("plone_utils") - - def is_email(email): - if not plone_utils.validateSingleEmailAddress(email): - return False - return True - - def recipient_from_contact(contact): - if not contact: - return None - email = contact.getEmailAddress() - return { - "UID": api.get_uid(contact), - "Username": contact.getUsername(), - "Fullname": to_utf8(contact.Title()), - "EmailAddress": email, - } - - def recipient_from_email(email): - if not is_email(email): - return None - return { - "UID": "", - "Username": "", - "Fullname": email, - "EmailAddress": email, - } - - # Primary Contacts - to = filter(None, [recipient_from_contact(ar.getContact())]) - # CC Contacts - cc = filter(None, map(recipient_from_contact, ar.getCCContact())) - # CC Emails - cc_emails = ar.getCCEmails(as_list=True) - cc_emails = filter(None, map(recipient_from_email, cc_emails)) - - return to + cc + cc_emails diff --git a/src/bika/coa/browser/publish/templates/email.pt b/src/bika/coa/browser/publish/templates/email.pt index eea461d..9d70b44 100644 --- a/src/bika/coa/browser/publish/templates/email.pt +++ b/src/bika/coa/browser/publish/templates/email.pt @@ -143,7 +143,7 @@ -
+
@@ -179,30 +179,32 @@ - diff --git a/src/bika/coa/browser/utils.py b/src/bika/coa/browser/utils.py new file mode 100644 index 0000000..921cf79 --- /dev/null +++ b/src/bika/coa/browser/utils.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +import os +from senaite.impress.template import TemplateFinder as TF + + +class TemplateFinder(TF): + + def get_templates(self, extensions=[".pt", ".html"]): + templates = [] + for resource in self.resources: + name = resource["name"] + path = resource["path"] + contents = resource["contents"] or [] + for content in contents: + basename, ext = os.path.splitext(content) + if ext not in extensions: + continue + if basename.lower().startswith("example"): + continue + template = content + if name: + template = u"{}:{}".format(name, content) + template_path = os.path.join(path, content) + templates.append((template, template_path)) + templates.sort(key=lambda x: x[0]) + return templates diff --git a/src/bika/coa/configure.zcml b/src/bika/coa/configure.zcml index da62a60..474db45 100644 --- a/src/bika/coa/configure.zcml +++ b/src/bika/coa/configure.zcml @@ -44,6 +44,7 @@ + diff --git a/src/bika/coa/extenders/method.py b/src/bika/coa/extenders/method.py index 6ba691c..30646d9 100755 --- a/src/bika/coa/extenders/method.py +++ b/src/bika/coa/extenders/method.py @@ -5,7 +5,7 @@ from bika.lims.interfaces import IMethod from zope.component import adapts from zope.interface import implements -from bika.lims.browser.widgets import ReferenceWidget as BikaReferenceWidget +from senaite.core.browser.widgets import ReferenceWidget as BikaReferenceWidget class MethodSchemaExtender(object): diff --git a/src/bika/coa/reports/AWTCBatchMulti.pt b/src/bika/coa/reports/AWTCBatchMulti.pt new file mode 100644 index 0000000..0921e09 --- /dev/null +++ b/src/bika/coa/reports/AWTCBatchMulti.pt @@ -0,0 +1,671 @@ + + + +
+
Custom Report Options
+ +
+
+
+ +
+ +
+ + Number of attachments rendered within one row per Analysis Request + +
+
+ + + + + + + + + +
+

Please make sure all Samples are on the same batch

+
+
+ + + + +
+ +
+ +

Certificate of Analysis

+

+

+
+ + +
+ +
+
+ + + + + + + + + + +
+
+ +
+

+
This Analysis Request has been invalidated due to erroneously published results
+ + This Analysis request has been replaced by + + +
+ +
+

+
Provisional report
+
+
+
+
+
+ + +

Results

+ + + +
+
+
Sample ID
+
+ +
+
+
+ +
+
+
Client Sample ID
+
+ +
+
+
+ +
+
+
Sample Type
+
+ +
+
+
+ +
+
+
Sample Point
+
+ +
+
+
+ +
+
+
Date Sampled
+
+ +
+
+
+ +
+
+
Date Received
+
+ +
+
+
+ +
+
+
Date Verified
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+ + + + +
+ + - + + + + + + +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+

Results Interpretation for

+ +

Department

+
+
+
+
+
+
+ + + + +
+
+

Remarks for

+
+
+
+
+
+
+
+
+ + + +
+ + +

+ Attachments for +

+ + + + + + + +
+
+ +
+
+ Attachment for + +
+
+ +
+
+ Filename: + +
+
+
+
+
+
+
+
+ + + + +
+
+

Managers Responsible

+ + + + + + + + + +
+
+ +
+
+ +   + +
+
+ +
+
+ +
+
+
Published by
+
+ +
+
+
+
+
+
+ + + +
+
+
+ + + + Result out of client specified range. +
+
+ Not invoiced +
+
+ + + + Methods included in the + + schedule of Accreditation for this Laboratory. Analysis remarks are not + accredited + +
+
+ Analysis results relate only to the samples tested. +
+ +
+ Test results are at a % confidence level +
+
+
+
+ + + +