1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE |
4
|
|
|
# |
5
|
|
|
# Copyright 2018 by it's authors. |
6
|
|
|
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. |
7
|
|
|
|
8
|
|
|
|
9
|
|
|
from AccessControl import ClassSecurityInfo |
10
|
|
|
from Products.ATExtensions.ateapi import RecordsField |
11
|
|
|
from Products.Archetypes.Field import TextField |
12
|
|
|
from Products.Archetypes.Widget import RichWidget |
13
|
|
|
from Products.Archetypes.atapi import BooleanField |
14
|
|
|
from Products.Archetypes.atapi import BooleanWidget |
15
|
|
|
from Products.Archetypes.atapi import DecimalWidget |
16
|
|
|
from Products.Archetypes.atapi import FixedPointField |
17
|
|
|
from Products.Archetypes.atapi import IntegerField |
18
|
|
|
from Products.Archetypes.atapi import IntegerWidget |
19
|
|
|
from Products.Archetypes.atapi import LinesField |
20
|
|
|
from Products.Archetypes.atapi import MultiSelectionWidget |
21
|
|
|
from Products.Archetypes.atapi import ReferenceField |
22
|
|
|
from Products.Archetypes.atapi import Schema |
23
|
|
|
from Products.Archetypes.atapi import SelectionWidget |
24
|
|
|
from Products.Archetypes.atapi import StringField |
25
|
|
|
from Products.Archetypes.atapi import TextAreaWidget |
26
|
|
|
from Products.Archetypes.atapi import registerType |
27
|
|
|
from Products.Archetypes.utils import DisplayList |
28
|
|
|
from Products.Archetypes.utils import IntDisplayList |
29
|
|
|
from Products.CMFCore.utils import getToolByName |
30
|
|
|
from archetypes.referencebrowserwidget import ReferenceBrowserWidget |
31
|
|
|
from bika.lims import bikaMessageFactory as _ |
32
|
|
|
from bika.lims.browser.fields import DurationField |
33
|
|
|
from bika.lims.browser.widgets import DurationWidget |
34
|
|
|
from bika.lims.browser.widgets import RecordsWidget |
35
|
|
|
from bika.lims.browser.widgets import RejectionSetupWidget |
36
|
|
|
from bika.lims.config import ARIMPORT_OPTIONS |
37
|
|
|
from bika.lims.config import ATTACHMENT_OPTIONS |
38
|
|
|
from bika.lims.config import CURRENCIES |
39
|
|
|
from bika.lims.config import DECIMAL_MARKS |
40
|
|
|
from bika.lims.config import MULTI_VERIFICATION_TYPE |
41
|
|
|
from bika.lims.config import PROJECTNAME |
42
|
|
|
from bika.lims.config import SCINOTATION_OPTIONS |
43
|
|
|
from bika.lims.config import WORKSHEET_LAYOUT_OPTIONS |
44
|
|
|
from bika.lims.content.bikaschema import BikaFolderSchema |
45
|
|
|
from bika.lims.interfaces import IBikaSetup |
46
|
|
|
from bika.lims.interfaces import IHaveNoBreadCrumbs |
47
|
|
|
from bika.lims.locales import COUNTRIES |
48
|
|
|
from bika.lims.numbergenerator import INumberGenerator |
49
|
|
|
from bika.lims.vocabularies import getStickerTemplates as _getStickerTemplates |
50
|
|
|
from plone.app.folder import folder |
51
|
|
|
from zope.component import getUtility |
52
|
|
|
from zope.interface import implements |
53
|
|
|
|
54
|
|
|
|
55
|
|
|
class IDFormattingField(RecordsField): |
56
|
|
|
"""A list of prefixes per portal_type |
57
|
|
|
""" |
58
|
|
|
_properties = RecordsField._properties.copy() |
59
|
|
|
_properties.update({ |
60
|
|
|
'type': 'prefixes', |
61
|
|
|
'subfields': ( |
62
|
|
|
'portal_type', |
63
|
|
|
'form', |
64
|
|
|
'sequence_type', |
65
|
|
|
'context', |
66
|
|
|
'counter_type', |
67
|
|
|
'counter_reference', |
68
|
|
|
'prefix', |
69
|
|
|
'split_length' |
70
|
|
|
), |
71
|
|
|
'subfield_labels': { |
72
|
|
|
'portal_type': 'Portal Type', |
73
|
|
|
'form': 'Format', |
74
|
|
|
'sequence_type': 'Seq Type', |
75
|
|
|
'context': 'Context', |
76
|
|
|
'counter_type': 'Counter Type', |
77
|
|
|
'counter_reference': 'Counter Ref', |
78
|
|
|
'prefix': 'Prefix', |
79
|
|
|
'split_length': 'Split Length', |
80
|
|
|
}, |
81
|
|
|
'subfield_readonly': { |
82
|
|
|
'portal_type': False, |
83
|
|
|
'form': False, |
84
|
|
|
'sequence_type': False, |
85
|
|
|
'context': False, |
86
|
|
|
'counter_type': False, |
87
|
|
|
'counter_reference': False, |
88
|
|
|
'prefix': False, |
89
|
|
|
'split_length': False, |
90
|
|
|
}, |
91
|
|
|
'subfield_sizes': { |
92
|
|
|
'portal_type': 20, |
93
|
|
|
'form': 30, |
94
|
|
|
'sequence_type': 1, |
95
|
|
|
'context': 12, |
96
|
|
|
'counter_type': 1, |
97
|
|
|
'counter_reference': 12, |
98
|
|
|
'prefix': 12, |
99
|
|
|
'split_length': 5, |
100
|
|
|
}, |
101
|
|
|
'subfield_types': { |
102
|
|
|
'sequence_type': 'selection', |
103
|
|
|
'counter_type': 'selection', |
104
|
|
|
'split_length': 'int', |
105
|
|
|
}, |
106
|
|
|
'subfield_vocabularies': { |
107
|
|
|
'sequence_type': 'getSequenceTypes', |
108
|
|
|
'counter_type': 'getCounterTypes', |
109
|
|
|
}, |
110
|
|
|
'subfield_maxlength': { |
111
|
|
|
'form': 256, |
112
|
|
|
}, |
113
|
|
|
}) |
114
|
|
|
|
115
|
|
|
security = ClassSecurityInfo() |
116
|
|
|
|
117
|
|
|
def getSequenceTypes(self, instance=None): |
118
|
|
|
return DisplayList([ |
119
|
|
|
('', ''), |
120
|
|
|
('counter', 'Counter'), |
121
|
|
|
('generated', 'Generated') |
122
|
|
|
]) |
123
|
|
|
|
124
|
|
|
def getCounterTypes(self, instance=None): |
125
|
|
|
return DisplayList([ |
126
|
|
|
('', ''), |
127
|
|
|
('backreference', 'Backreference'), |
128
|
|
|
('contained', 'Contained') |
129
|
|
|
]) |
130
|
|
|
|
131
|
|
|
|
132
|
|
|
STICKER_AUTO_OPTIONS = DisplayList(( |
133
|
|
|
('None', _('None')), |
134
|
|
|
('register', _('Register')), |
135
|
|
|
('receive', _('Receive')), |
136
|
|
|
)) |
137
|
|
|
|
138
|
|
|
|
139
|
|
|
schema = BikaFolderSchema.copy() + Schema(( |
140
|
|
|
IntegerField( |
141
|
|
|
'PasswordLifetime', |
142
|
|
|
schemata="Security", |
143
|
|
|
required=1, |
144
|
|
|
default=0, |
145
|
|
|
widget=IntegerWidget( |
146
|
|
|
label=_("Password lifetime"), |
147
|
|
|
description=_("The number of days before a password expires. 0 disables password expiry"), |
148
|
|
|
) |
149
|
|
|
), |
150
|
|
|
IntegerField( |
151
|
|
|
'AutoLogOff', |
152
|
|
|
schemata="Security", |
153
|
|
|
required=1, |
154
|
|
|
default=0, |
155
|
|
|
widget=IntegerWidget( |
156
|
|
|
label=_("Automatic log-off"), |
157
|
|
|
description=_( |
158
|
|
|
"The number of minutes before a user is automatically logged off. " |
159
|
|
|
"0 disables automatic log-off"), |
160
|
|
|
) |
161
|
|
|
), |
162
|
|
|
BooleanField( |
163
|
|
|
'RestrictWorksheetUsersAccess', |
164
|
|
|
schemata="Security", |
165
|
|
|
default=True, |
166
|
|
|
widget=BooleanWidget( |
167
|
|
|
label=_("Allow access to worksheets only to assigned analysts"), |
168
|
|
|
description=_("If unchecked, analysts will have access to all worksheets.") |
169
|
|
|
) |
170
|
|
|
), |
171
|
|
|
BooleanField( |
172
|
|
|
'AllowToSubmitNotAssigned', |
173
|
|
|
schemata="Security", |
174
|
|
|
default=True, |
175
|
|
|
widget=BooleanWidget( |
176
|
|
|
label=_("Allow to submit results for unassigned analyses or for " |
177
|
|
|
"analyses assigned to others"), |
178
|
|
|
description=_( |
179
|
|
|
"If unchecked, users will only be able to submit results " |
180
|
|
|
"for the analyses they are assigned to, and the submission of " |
181
|
|
|
"results for unassigned analyses won't be permitted. This " |
182
|
|
|
"setting does not apply to users with role Lab Manager") |
183
|
|
|
) |
184
|
|
|
), |
185
|
|
|
BooleanField( |
186
|
|
|
'RestrictWorksheetManagement', |
187
|
|
|
schemata="Security", |
188
|
|
|
default=True, |
189
|
|
|
widget=BooleanWidget( |
190
|
|
|
label=_("Only lab managers can create and manage worksheets"), |
191
|
|
|
description=_("If unchecked, analysts and lab clerks will " |
192
|
|
|
"be able to manage Worksheets, too. If the " |
193
|
|
|
"users have restricted access only to those " |
194
|
|
|
"worksheets for which they are assigned, " |
195
|
|
|
"this option will be checked and readonly.") |
196
|
|
|
) |
197
|
|
|
), |
198
|
|
|
BooleanField( |
199
|
|
|
'ShowPrices', |
200
|
|
|
schemata="Accounting", |
201
|
|
|
default=True, |
202
|
|
|
widget=BooleanWidget( |
203
|
|
|
label=_("Include and display pricing information"), |
204
|
|
|
) |
205
|
|
|
), |
206
|
|
|
StringField( |
207
|
|
|
'Currency', |
208
|
|
|
schemata="Accounting", |
209
|
|
|
required=1, |
210
|
|
|
vocabulary=CURRENCIES, |
211
|
|
|
default='ZAR', |
212
|
|
|
widget=SelectionWidget( |
213
|
|
|
label=_("Currency"), |
214
|
|
|
description=_("Select the currency the site will use to display prices."), |
215
|
|
|
format='select', |
216
|
|
|
) |
217
|
|
|
), |
218
|
|
|
StringField( |
219
|
|
|
'DefaultCountry', |
220
|
|
|
schemata="Accounting", |
221
|
|
|
required=1, |
222
|
|
|
vocabulary='getCountries', |
223
|
|
|
default='', |
224
|
|
|
widget=SelectionWidget( |
225
|
|
|
label=_("Country"), |
226
|
|
|
description=_("Select the country the site will show by default"), |
227
|
|
|
format='select', |
228
|
|
|
) |
229
|
|
|
), |
230
|
|
|
FixedPointField( |
231
|
|
|
'MemberDiscount', |
232
|
|
|
schemata="Accounting", |
233
|
|
|
default='33.33', |
234
|
|
|
widget=DecimalWidget( |
235
|
|
|
label=_("Member discount %"), |
236
|
|
|
description=_( |
237
|
|
|
"The discount percentage entered here, is applied to the prices for clients " |
238
|
|
|
"flagged as 'members', normally co-operative members or associates deserving " |
239
|
|
|
"of this discount"), |
240
|
|
|
) |
241
|
|
|
), |
242
|
|
|
FixedPointField( |
243
|
|
|
'VAT', |
244
|
|
|
schemata="Accounting", |
245
|
|
|
default='14.00', |
246
|
|
|
widget=DecimalWidget( |
247
|
|
|
label=_("VAT %"), |
248
|
|
|
description=_( |
249
|
|
|
"Enter percentage value eg. 14.0. This percentage is applied system wide " |
250
|
|
|
"but can be overwrittem on individual items"), |
251
|
|
|
) |
252
|
|
|
), |
253
|
|
|
StringField( |
254
|
|
|
'DecimalMark', |
255
|
|
|
schemata="Results Reports", |
256
|
|
|
vocabulary=DECIMAL_MARKS, |
257
|
|
|
default=".", |
258
|
|
|
widget=SelectionWidget( |
259
|
|
|
label=_("Default decimal mark"), |
260
|
|
|
description=_("Preferred decimal mark for reports."), |
261
|
|
|
format='select', |
262
|
|
|
) |
263
|
|
|
), |
264
|
|
|
StringField( |
265
|
|
|
'ScientificNotationReport', |
266
|
|
|
schemata="Results Reports", |
267
|
|
|
default='1', |
268
|
|
|
vocabulary=SCINOTATION_OPTIONS, |
269
|
|
|
widget=SelectionWidget( |
270
|
|
|
label=_("Default scientific notation format for reports"), |
271
|
|
|
description=_("Preferred scientific notation format for reports"), |
272
|
|
|
format='select', |
273
|
|
|
) |
274
|
|
|
), |
275
|
|
|
IntegerField( |
276
|
|
|
'MinimumResults', |
277
|
|
|
schemata="Results Reports", |
278
|
|
|
required=1, |
279
|
|
|
default=5, |
280
|
|
|
widget=IntegerWidget( |
281
|
|
|
label=_("Minimum number of results for QC stats calculations"), |
282
|
|
|
description=_( |
283
|
|
|
"Using too few data points does not make statistical sense. " |
284
|
|
|
"Set an acceptable minimum number of results before QC statistics " |
285
|
|
|
"will be calculated and plotted"), |
286
|
|
|
) |
287
|
|
|
), |
288
|
|
|
BooleanField( |
289
|
|
|
'CategoriseAnalysisServices', |
290
|
|
|
schemata="Analyses", |
291
|
|
|
default=False, |
292
|
|
|
widget=BooleanWidget( |
293
|
|
|
label=_("Categorise analysis services"), |
294
|
|
|
description=_("Group analysis services by category in the LIMS tables, helpful when the list is long") |
295
|
|
|
), |
296
|
|
|
), |
297
|
|
|
BooleanField( |
298
|
|
|
'EnableARSpecs', |
299
|
|
|
schemata="Analyses", |
300
|
|
|
default=False, |
301
|
|
|
widget=BooleanWidget( |
302
|
|
|
label=_("Enable Sample Specifications"), |
303
|
|
|
description=_( |
304
|
|
|
"Analysis specifications which are edited directly on the " |
305
|
|
|
"Sample."), |
306
|
|
|
), |
307
|
|
|
), |
308
|
|
|
IntegerField( |
309
|
|
|
'ExponentialFormatThreshold', |
310
|
|
|
schemata="Analyses", |
311
|
|
|
required=1, |
312
|
|
|
default=7, |
313
|
|
|
widget=IntegerWidget( |
314
|
|
|
label=_("Exponential format threshold"), |
315
|
|
|
description=_( |
316
|
|
|
"Result values with at least this number of significant " |
317
|
|
|
"digits are displayed in scientific notation using the " |
318
|
|
|
"letter 'e' to indicate the exponent. The precision can be " |
319
|
|
|
"configured in individual Analysis Services."), |
320
|
|
|
) |
321
|
|
|
), |
322
|
|
|
BooleanField( |
323
|
|
|
'EnableAnalysisRemarks', |
324
|
|
|
schemata="Analyses", |
325
|
|
|
default=False, |
326
|
|
|
widget=BooleanWidget( |
327
|
|
|
label=_("Add a remarks field to all analyses"), |
328
|
|
|
description=_( |
329
|
|
|
"If enabled, a free text field will be displayed close to " |
330
|
|
|
"each analysis in results entry view" |
331
|
|
|
) |
332
|
|
|
), |
333
|
|
|
), |
334
|
|
|
BooleanField( |
335
|
|
|
'SelfVerificationEnabled', |
336
|
|
|
schemata="Analyses", |
337
|
|
|
default=False, |
338
|
|
|
widget=BooleanWidget( |
339
|
|
|
label=_("Allow self-verification of results"), |
340
|
|
|
description=_( |
341
|
|
|
"If enabled, a user who submitted a result will also be able " |
342
|
|
|
"to verify it. This setting only take effect for those users " |
343
|
|
|
"with a role assigned that allows them to verify results " |
344
|
|
|
"(by default, managers, labmanagers and verifiers)." |
345
|
|
|
"This setting can be overrided for a given Analysis in " |
346
|
|
|
"Analysis Service edit view. By default, disabled."), |
347
|
|
|
), |
348
|
|
|
), |
349
|
|
|
IntegerField( |
350
|
|
|
'NumberOfRequiredVerifications', |
351
|
|
|
schemata="Analyses", |
352
|
|
|
default=1, |
353
|
|
|
vocabulary="_getNumberOfRequiredVerificationsVocabulary", |
354
|
|
|
widget=SelectionWidget( |
355
|
|
|
format="select", |
356
|
|
|
label=_("Number of required verifications"), |
357
|
|
|
description=_( |
358
|
|
|
"Number of required verifications before a given result being " |
359
|
|
|
"considered as 'verified'. This setting can be overrided for " |
360
|
|
|
"any Analysis in Analysis Service edit view. By default, 1"), |
361
|
|
|
), |
362
|
|
|
), |
363
|
|
|
StringField( |
364
|
|
|
'TypeOfmultiVerification', |
365
|
|
|
schemata="Analyses", |
366
|
|
|
default='self_multi_enabled', |
367
|
|
|
vocabulary=MULTI_VERIFICATION_TYPE, |
368
|
|
|
widget=SelectionWidget( |
369
|
|
|
label=_("Multi Verification type"), |
370
|
|
|
description=_( |
371
|
|
|
"Choose type of multiple verification for the same user." |
372
|
|
|
"This setting can enable/disable verifying/consecutively verifying" |
373
|
|
|
"more than once for the same user."), |
374
|
|
|
format='select', |
375
|
|
|
) |
376
|
|
|
), |
377
|
|
|
LinesField( |
378
|
|
|
'ARImportOption', |
379
|
|
|
schemata="Analyses", |
380
|
|
|
vocabulary=ARIMPORT_OPTIONS, |
381
|
|
|
widget=MultiSelectionWidget( |
382
|
|
|
visible=False, |
383
|
|
|
label=_("AR Import options"), |
384
|
|
|
description=_( |
385
|
|
|
"'Classic' indicates importing samples per sample and " |
386
|
|
|
"analysis service selection. With 'Profiles', analysis profile keywords " |
387
|
|
|
"are used to select multiple analysis services together"), |
388
|
|
|
) |
389
|
|
|
), |
390
|
|
|
StringField( |
391
|
|
|
'ARAttachmentOption', |
392
|
|
|
schemata="Analyses", |
393
|
|
|
default='p', |
394
|
|
|
vocabulary=ATTACHMENT_OPTIONS, |
395
|
|
|
widget=SelectionWidget( |
396
|
|
|
format='select', |
397
|
|
|
label=_("Sample Attachment Option"), |
398
|
|
|
description=_( |
399
|
|
|
"The system wide default configuration to indicate " |
400
|
|
|
"whether file attachments are required, permitted or not " |
401
|
|
|
"per sample"), |
402
|
|
|
) |
403
|
|
|
), |
404
|
|
|
StringField( |
405
|
|
|
'AnalysisAttachmentOption', |
406
|
|
|
schemata="Analyses", |
407
|
|
|
default='p', |
408
|
|
|
vocabulary=ATTACHMENT_OPTIONS, |
409
|
|
|
widget=SelectionWidget( |
410
|
|
|
format='select', |
411
|
|
|
label=_("Analysis Attachment Option"), |
412
|
|
|
description=_( |
413
|
|
|
"Same as the above, but sets the default on analysis services. " |
414
|
|
|
"This setting can be set per individual analysis on its " |
415
|
|
|
"own configuration"), |
416
|
|
|
) |
417
|
|
|
), |
418
|
|
|
StringField( |
419
|
|
|
'ResultsDecimalMark', |
420
|
|
|
schemata="Analyses", |
421
|
|
|
vocabulary=DECIMAL_MARKS, |
422
|
|
|
default=".", |
423
|
|
|
widget=SelectionWidget( |
424
|
|
|
label=_("Default decimal mark"), |
425
|
|
|
description=_("Preferred decimal mark for results"), |
426
|
|
|
format='select', |
427
|
|
|
) |
428
|
|
|
), |
429
|
|
|
StringField( |
430
|
|
|
'ScientificNotationResults', |
431
|
|
|
schemata="Analyses", |
432
|
|
|
default='1', |
433
|
|
|
vocabulary=SCINOTATION_OPTIONS, |
434
|
|
|
widget=SelectionWidget( |
435
|
|
|
label=_("Default scientific notation format for results"), |
436
|
|
|
description=_("Preferred scientific notation format for results"), |
437
|
|
|
format='select', |
438
|
|
|
) |
439
|
|
|
), |
440
|
|
|
StringField( |
441
|
|
|
'WorksheetLayout', |
442
|
|
|
schemata="Appearance", |
443
|
|
|
default='1', |
444
|
|
|
vocabulary=WORKSHEET_LAYOUT_OPTIONS, |
445
|
|
|
widget=SelectionWidget( |
446
|
|
|
label=_("Default layout in worksheet view"), |
447
|
|
|
description=_("Preferred layout of the results entry table " |
448
|
|
|
"in the Worksheet view. Classic layout displays " |
449
|
|
|
"the Samples in rows and the analyses " |
450
|
|
|
"in columns. Transposed layout displays the " |
451
|
|
|
"Samples in columns and the analyses " |
452
|
|
|
"in rows."), |
453
|
|
|
format='select', |
454
|
|
|
) |
455
|
|
|
), |
456
|
|
|
BooleanField( |
457
|
|
|
'DashboardByDefault', |
458
|
|
|
schemata="Appearance", |
459
|
|
|
default=True, |
460
|
|
|
widget=BooleanWidget( |
461
|
|
|
label=_("Use Dashboard as default front page"), |
462
|
|
|
description=_("Select this to activate the dashboard as a default front page.") |
463
|
|
|
), |
464
|
|
|
), |
465
|
|
|
ReferenceField( |
466
|
|
|
'LandingPage', |
467
|
|
|
schemata="Appearance", |
468
|
|
|
multiValued=0, |
469
|
|
|
allowed_types=('Document', ), |
470
|
|
|
relationship='SetupLandingPage', |
471
|
|
|
widget=ReferenceBrowserWidget( |
472
|
|
|
label=_("Landing Page"), |
473
|
|
|
description=_("The selected landing page is displayed for non-authenticated users " |
474
|
|
|
"and if the Dashboard is not selected as the default front page. " |
475
|
|
|
"If no landing page is selected, the default Bika frontpage is displayed."), |
476
|
|
|
allow_search=1, |
477
|
|
|
allow_browse=1, |
478
|
|
|
startup_directory='/', |
479
|
|
|
force_close_on_insert=1, |
480
|
|
|
default_search_index='SearchableText', |
481
|
|
|
base_query={'review_state': 'published'}, |
482
|
|
|
), |
483
|
|
|
), |
484
|
|
|
BooleanField( |
485
|
|
|
'PrintingWorkflowEnabled', |
486
|
|
|
schemata="Sampling", |
487
|
|
|
default=False, |
488
|
|
|
widget=BooleanWidget( |
489
|
|
|
label=_("Enable the Results Report Printing workflow"), |
490
|
|
|
description=_("Select this to allow the user to set an " |
491
|
|
|
"additional 'Printed' status to those Analysis " |
492
|
|
|
"Requests tha have been Published. " |
493
|
|
|
"Disabled by default.") |
494
|
|
|
), |
495
|
|
|
), |
496
|
|
|
BooleanField( |
497
|
|
|
'SamplingWorkflowEnabled', |
498
|
|
|
schemata="Sampling", |
499
|
|
|
default=False, |
500
|
|
|
widget=BooleanWidget( |
501
|
|
|
label=_("Enable Sampling"), |
502
|
|
|
description=_("Select this to activate the sample collection workflow steps.") |
503
|
|
|
), |
504
|
|
|
), |
505
|
|
|
BooleanField( |
506
|
|
|
'ScheduleSamplingEnabled', |
507
|
|
|
schemata="Sampling", |
508
|
|
|
default=False, |
509
|
|
|
widget=BooleanWidget( |
510
|
|
|
label=_("Enable Sampling Scheduling"), |
511
|
|
|
description=_( |
512
|
|
|
"Select this to allow a Sampling Coordinator to" + |
513
|
|
|
" schedule a sampling. This functionality only takes effect" + |
514
|
|
|
" when 'Sampling workflow' is active") |
515
|
|
|
), |
516
|
|
|
), |
517
|
|
|
BooleanField( |
518
|
|
|
'ShowPartitions', |
519
|
|
|
schemata="Sampling", |
520
|
|
|
default=True, |
521
|
|
|
widget=BooleanWidget( |
522
|
|
|
label=_("Display individual sample partitions "), |
523
|
|
|
description=_("Turn this on if you want to work with sample partitions") |
524
|
|
|
), |
525
|
|
|
), |
526
|
|
|
BooleanField( |
527
|
|
|
'SamplePreservationEnabled', |
528
|
|
|
schemata="Sampling", |
529
|
|
|
default=False, |
530
|
|
|
widget=BooleanWidget( |
531
|
|
|
label=_("Enable Sample Preservation"), |
532
|
|
|
description=_("") |
533
|
|
|
), |
534
|
|
|
), |
535
|
|
|
DurationField( |
536
|
|
|
'DefaultTurnaroundTime', |
537
|
|
|
schemata="Sampling", |
538
|
|
|
required=1, |
539
|
|
|
default={"days": 5, "hours": 0, "minutes": 0}, |
540
|
|
|
widget=DurationWidget( |
541
|
|
|
label=_("Default turnaround time for analyses."), |
542
|
|
|
description=_( |
543
|
|
|
"This is the default maximum time allowed for performing " |
544
|
|
|
"analyses. It is only used for analyses where the analysis " |
545
|
|
|
"service does not specify a turnaround time."), |
546
|
|
|
) |
547
|
|
|
), |
548
|
|
|
DurationField( |
549
|
|
|
'DefaultSampleLifetime', |
550
|
|
|
schemata="Sampling", |
551
|
|
|
required=1, |
552
|
|
|
default={"days": 30, "hours": 0, "minutes": 0}, |
553
|
|
|
widget=DurationWidget( |
554
|
|
|
label=_("Default sample retention period"), |
555
|
|
|
description=_( |
556
|
|
|
"The number of days before a sample expires and cannot be analysed " |
557
|
|
|
"any more. This setting can be overwritten per individual sample type " |
558
|
|
|
"in the sample types setup"), |
559
|
|
|
) |
560
|
|
|
), |
561
|
|
|
BooleanField( |
562
|
|
|
'NotifyOnSampleRejection', |
563
|
|
|
schemata="Notifications", |
564
|
|
|
default=False, |
565
|
|
|
widget=BooleanWidget( |
566
|
|
|
label=_("Email notification on Sample rejection"), |
567
|
|
|
description=_("Select this to activate automatic notifications " |
568
|
|
|
"via email to the Client when a Sample is rejected.") |
569
|
|
|
), |
570
|
|
|
), |
571
|
|
|
BooleanField( |
572
|
|
|
'NotifyOnSampleInvalidation', |
573
|
|
|
schemata="Notifications", |
574
|
|
|
default=True, |
575
|
|
|
widget=BooleanWidget( |
576
|
|
|
label=_("Email notification on Sample invalidation"), |
577
|
|
|
description=_("Select this to activate automatic notifications " |
578
|
|
|
"via email to the Client and Lab Managers when a " |
579
|
|
|
"Sample is invalidated.") |
580
|
|
|
), |
581
|
|
|
), |
582
|
|
|
TextField( |
583
|
|
|
"EmailBodySampleInvalidation", |
584
|
|
|
default_content_type='text/html', |
585
|
|
|
default_output_type='text/x-html-safe', |
586
|
|
|
schemata="Notifications", |
587
|
|
|
label=_("Email body for Sample Invalidation notifications"), |
588
|
|
|
default= |
589
|
|
|
"Some non-conformities have been detected in the results report " |
590
|
|
|
"published for Sample $sample_link. " |
591
|
|
|
"<br/><br/> " |
592
|
|
|
"A new Sample $retest_link has been created automatically, and the " |
593
|
|
|
"previous request has been invalidated. " |
594
|
|
|
"<br/><br/> " |
595
|
|
|
"The root cause is under investigation and corrective " |
596
|
|
|
"action has been initiated. " |
597
|
|
|
"<br/><br/> " |
598
|
|
|
"$lab_address", |
599
|
|
|
widget=RichWidget( |
600
|
|
|
label=_("Email body for Sample Invalidation notifications"), |
601
|
|
|
description=_("Set the text for the body of the email to be sent, " |
602
|
|
|
", if option 'Email notification on Sample " |
603
|
|
|
"'invalidation' enabled, to the Sample's client " |
604
|
|
|
"contact. You can use reserved keywords: $sample_id, " |
605
|
|
|
"$sample_link, $retest_id, $retest_link, " |
606
|
|
|
"$lab_address"), |
607
|
|
|
default_mime_type='text/x-rst', |
608
|
|
|
output_mime_type='text/x-html', |
609
|
|
|
allow_file_upload=False, |
610
|
|
|
rows=10, |
611
|
|
|
), |
612
|
|
|
), |
613
|
|
|
StringField( |
614
|
|
|
'AutoPrintStickers', |
615
|
|
|
schemata="Sticker", |
616
|
|
|
vocabulary=STICKER_AUTO_OPTIONS, |
617
|
|
|
widget=SelectionWidget( |
618
|
|
|
format='select', |
619
|
|
|
label=_("Automatic sticker printing"), |
620
|
|
|
description=_( |
621
|
|
|
"Select 'Register' if you want stickers to be automatically printed when " |
622
|
|
|
"new Samples or sample records are created. Select 'Receive' to print stickers " |
623
|
|
|
"when Samples or Samples are received. Select 'None' to disable automatic printing"), |
624
|
|
|
) |
625
|
|
|
), |
626
|
|
|
StringField( |
627
|
|
|
'AutoStickerTemplate', |
628
|
|
|
schemata="Sticker", |
629
|
|
|
vocabulary="getStickerTemplates", |
630
|
|
|
widget=SelectionWidget( |
631
|
|
|
format='select', |
632
|
|
|
label=_("Sticker templates"), |
633
|
|
|
description=_("Select which sticker to print when automatic sticker printing is enabled"), |
634
|
|
|
) |
635
|
|
|
), |
636
|
|
|
StringField( |
637
|
|
|
'SmallStickerTemplate', |
638
|
|
|
schemata="Sticker", |
639
|
|
|
vocabulary="getStickerTemplates", |
640
|
|
|
default="Code_128_1x48mm.pt", |
641
|
|
|
widget=SelectionWidget( |
642
|
|
|
format='select', |
643
|
|
|
label=_("Small sticker"), |
644
|
|
|
description=_("Select which sticker should be used as the 'small' sticker by default") |
645
|
|
|
) |
646
|
|
|
), |
647
|
|
|
StringField( |
648
|
|
|
'LargeStickerTemplate', |
649
|
|
|
schemata="Sticker", |
650
|
|
|
vocabulary="getStickerTemplates", |
651
|
|
|
default="Code_128_1x72mm.pt", |
652
|
|
|
widget=SelectionWidget( |
653
|
|
|
format='select', |
654
|
|
|
label=_("Large sticker"), |
655
|
|
|
description=_("Select which sticker should be used as the 'large' sticker by default") |
656
|
|
|
) |
657
|
|
|
), |
658
|
|
|
IntegerField( |
659
|
|
|
'DefaultNumberOfCopies', |
660
|
|
|
schemata="Sticker", |
661
|
|
|
required="1", |
662
|
|
|
default="1", |
663
|
|
|
widget=IntegerWidget( |
664
|
|
|
label=_("Number of copies"), |
665
|
|
|
description=_("Set the default number of copies to be printed for each sticker") |
666
|
|
|
) |
667
|
|
|
), |
668
|
|
|
IDFormattingField( |
669
|
|
|
'IDFormatting', |
670
|
|
|
schemata="ID Server", |
671
|
|
|
default=[ |
672
|
|
|
{ |
673
|
|
|
'form': 'AI-{seq:03d}', |
674
|
|
|
'portal_type': 'ARImport', |
675
|
|
|
'sequence_type': 'generated', |
676
|
|
|
'split_length': 1 |
677
|
|
|
}, { |
678
|
|
|
'form': 'B-{seq:03d}', |
679
|
|
|
'portal_type': 'Batch', |
680
|
|
|
'prefix': 'batch', |
681
|
|
|
'sequence_type': 'generated', |
682
|
|
|
'split_length': 1 |
683
|
|
|
}, { |
684
|
|
|
'form': 'D-{seq:03d}', |
685
|
|
|
'portal_type': 'DuplicateAnalysis', |
686
|
|
|
'prefix': 'duplicate', |
687
|
|
|
'sequence_type': 'generated', |
688
|
|
|
'split_length': 1 |
689
|
|
|
}, { |
690
|
|
|
'form': 'I-{seq:03d}', |
691
|
|
|
'portal_type': 'Invoice', |
692
|
|
|
'prefix': 'invoice', |
693
|
|
|
'sequence_type': 'generated', |
694
|
|
|
'split_length': 1 |
695
|
|
|
}, { |
696
|
|
|
'form': 'QC-{seq:03d}', |
697
|
|
|
'portal_type': 'ReferenceSample', |
698
|
|
|
'prefix': 'refsample', |
699
|
|
|
'sequence_type': 'generated', |
700
|
|
|
'split_length': 1 |
701
|
|
|
}, { |
702
|
|
|
'form': 'SA-{seq:03d}', |
703
|
|
|
'portal_type': 'ReferenceAnalysis', |
704
|
|
|
'prefix': 'refanalysis', |
705
|
|
|
'sequence_type': 'generated', |
706
|
|
|
'split_length': 1 |
707
|
|
|
}, { |
708
|
|
|
'form': 'WS-{seq:03d}', |
709
|
|
|
'portal_type': 'Worksheet', |
710
|
|
|
'prefix': 'worksheet', |
711
|
|
|
'sequence_type': 'generated', |
712
|
|
|
'split_length': 1 |
713
|
|
|
}, { |
714
|
|
|
'form': '{sampleType}-{seq:04d}', |
715
|
|
|
'portal_type': 'AnalysisRequest', |
716
|
|
|
'prefix': 'analysisrequest', |
717
|
|
|
'sequence_type': 'generated', |
718
|
|
|
'split_length': 1 |
719
|
|
|
}, { |
720
|
|
|
'form': '{parent_ar_id}-P{partition_count:02d}', |
721
|
|
|
'portal_type': 'AnalysisRequestPartition', |
722
|
|
|
'prefix': 'analysisrequestpartition', |
723
|
|
|
'sequence_type': '', |
724
|
|
|
'split-length': 1 |
725
|
|
|
}, { |
726
|
|
|
'form': '{parent_base_id}-R{retest_count:02d}', |
727
|
|
|
'portal_type': 'AnalysisRequestRetest', |
728
|
|
|
'prefix': 'analysisrequestretest', |
729
|
|
|
'sequence_type': '', |
730
|
|
|
'split-length': 1 |
731
|
|
|
}, { |
732
|
|
|
'form': '{parent_ar_id}-S{secondary_count:02d}', |
733
|
|
|
'portal_type': 'AnalysisRequestSecondary', |
734
|
|
|
'prefix': 'analysisrequestsecondary', |
735
|
|
|
'sequence_type': '', |
736
|
|
|
'split-length': 1 |
737
|
|
|
}, |
738
|
|
|
], |
739
|
|
|
widget=RecordsWidget( |
740
|
|
|
label=_("Formatting Configuration"), |
741
|
|
|
allowDelete=True, |
742
|
|
|
description=_( |
743
|
|
|
" <p>The Bika LIMS ID Server provides unique sequential IDs " |
744
|
|
|
"for objects such as Samples and Worksheets etc, based on a " |
745
|
|
|
"format specified for each content type.</p>" |
746
|
|
|
"<p>The format is constructed similarly to the Python format" |
747
|
|
|
" syntax, using predefined variables per content type, and" |
748
|
|
|
" advancing the IDs through a sequence number, 'seq' and its" |
749
|
|
|
" padding as a number of digits, e.g. '03d' for a sequence of" |
750
|
|
|
" IDs from 001 to 999.</p>" |
751
|
|
|
"<p>Alphanumeric prefixes for IDs are included as is in the" |
752
|
|
|
" formats, e.g. WS for Worksheet in WS-{seq:03d} produces" |
753
|
|
|
" sequential Worksheet IDs: WS-001, WS-002, WS-003 etc.</p>" |
754
|
|
|
"<p>For dynamic generation of alphanumeric and sequential IDs," |
755
|
|
|
" the wildcard {alpha} can be used. E.g WS-{alpha:2a3d}" |
756
|
|
|
" produces WS-AA001, WS-AA002, WS-AB034, etc.</p>" |
757
|
|
|
"<p>Variables that can be used include:" |
758
|
|
|
"<table>" |
759
|
|
|
"<tr>" |
760
|
|
|
"<th style='width:150px'>Content Type</th><th>Variables</th>" |
761
|
|
|
"</tr>" |
762
|
|
|
"<tr><td>Client</td><td>{client}</td></tr>" |
763
|
|
|
"<tr><td>Year</td><td>{year}</td></tr>" |
764
|
|
|
"<tr><td>Sample ID</td><td>{sampleId}</td></tr>" |
765
|
|
|
"<tr><td>Sample Type</td><td>{sampleType}</td></tr>" |
766
|
|
|
"<tr><td>Sampling Date</td><td>{samplingDate}</td></tr>" |
767
|
|
|
"<tr><td>Date Sampled</td><td>{dateSampled}</td></tr>" |
768
|
|
|
"</table>" |
769
|
|
|
"</p>" |
770
|
|
|
"<p>Configuration Settings:" |
771
|
|
|
"<ul>" |
772
|
|
|
"<li>format:" |
773
|
|
|
"<ul><li>a python format string constructed from predefined" |
774
|
|
|
" variables like sampleId, client, sampleType.</li>" |
775
|
|
|
"<li>special variable 'seq' must be positioned last in the" |
776
|
|
|
"format string</li></ul></li>" |
777
|
|
|
"<li>sequence type: [generated|counter]</li>" |
778
|
|
|
"<li>context: if type counter, provides context the counting" |
779
|
|
|
" function</li>" |
780
|
|
|
"<li>counter type: [backreference|contained]</li>" |
781
|
|
|
"<li>counter reference: a parameter to the counting" |
782
|
|
|
" function</li>" |
783
|
|
|
"<li>prefix: default prefix if none provided in format" |
784
|
|
|
" string</li>" |
785
|
|
|
"<li>split length: the number of parts to be included in the" |
786
|
|
|
" prefix</li>" |
787
|
|
|
"</ul></p>") |
788
|
|
|
) |
789
|
|
|
), |
790
|
|
|
StringField( |
791
|
|
|
'IDServerValues', |
792
|
|
|
schemata="ID Server", |
793
|
|
|
accessor="getIDServerValuesHTML", |
794
|
|
|
readonly=True, |
795
|
|
|
widget=TextAreaWidget( |
796
|
|
|
label=_("ID Server Values"), |
797
|
|
|
cols=30, |
798
|
|
|
rows=30, |
799
|
|
|
), |
800
|
|
|
), |
801
|
|
|
RecordsField( |
802
|
|
|
'RejectionReasons', |
803
|
|
|
schemata="Analyses", |
804
|
|
|
widget=RejectionSetupWidget( |
805
|
|
|
label=_("Enable the rejection workflow"), |
806
|
|
|
description=_("Select this to activate the rejection workflow " |
807
|
|
|
"for Samples. A 'Reject' option will be displayed in " |
808
|
|
|
"the actions menu.") |
809
|
|
|
), |
810
|
|
|
), |
811
|
|
|
IntegerField( |
812
|
|
|
'DefaultNumberOfARsToAdd', |
813
|
|
|
schemata="Analyses", |
814
|
|
|
required=0, |
815
|
|
|
default=4, |
816
|
|
|
widget=IntegerWidget( |
817
|
|
|
label=_("Default count of Sample to add."), |
818
|
|
|
description=_("Default value of the 'Sample count' when users click 'ADD' button to create new Samples"), |
819
|
|
|
) |
820
|
|
|
), |
821
|
|
|
)) |
822
|
|
|
|
823
|
|
|
schema['title'].validators = () |
824
|
|
|
schema['title'].widget.visible = False |
825
|
|
|
# Update the validation layer after change the validator in runtime |
826
|
|
|
schema['title']._validationLayer() |
827
|
|
|
|
828
|
|
|
|
829
|
|
|
class BikaSetup(folder.ATFolder): |
830
|
|
|
"""LIMS Setup |
831
|
|
|
""" |
832
|
|
|
implements(IBikaSetup, IHaveNoBreadCrumbs) |
833
|
|
|
|
834
|
|
|
schema = schema |
|
|
|
|
835
|
|
|
security = ClassSecurityInfo() |
836
|
|
|
|
837
|
|
|
def getAttachmentsPermitted(self): |
838
|
|
|
"""Attachments permitted |
839
|
|
|
""" |
840
|
|
|
if self.getARAttachmentOption() in ['r', 'p'] \ |
841
|
|
|
or self.getAnalysisAttachmentOption() in ['r', 'p']: |
842
|
|
|
return True |
843
|
|
|
else: |
844
|
|
|
return False |
845
|
|
|
|
846
|
|
|
def getStickerTemplates(self): |
847
|
|
|
"""Get the sticker templates |
848
|
|
|
""" |
849
|
|
|
out = [[t['id'], t['title']] for t in _getStickerTemplates()] |
850
|
|
|
return DisplayList(out) |
851
|
|
|
|
852
|
|
|
def getARAttachmentsPermitted(self): |
853
|
|
|
"""AR attachments permitted |
854
|
|
|
""" |
855
|
|
|
if self.getARAttachmentOption() == 'n': |
856
|
|
|
return False |
857
|
|
|
else: |
858
|
|
|
return True |
859
|
|
|
|
860
|
|
|
def getAnalysisAttachmentsPermitted(self): |
861
|
|
|
"""Analysis attachments permitted |
862
|
|
|
""" |
863
|
|
|
if self.getAnalysisAttachmentOption() == 'n': |
864
|
|
|
return False |
865
|
|
|
else: |
866
|
|
|
return True |
867
|
|
|
|
868
|
|
|
def getAnalysisServicesVocabulary(self): |
869
|
|
|
""" |
870
|
|
|
Get all active Analysis Services from Bika Setup and return them as Display List. |
871
|
|
|
""" |
872
|
|
|
bsc = getToolByName(self, 'bika_setup_catalog') |
873
|
|
|
brains = bsc(portal_type='AnalysisService', |
874
|
|
|
is_active=True) |
875
|
|
|
items = [(b.UID, b.Title) for b in brains] |
876
|
|
|
items.insert(0, ("", "")) |
877
|
|
|
items.sort(lambda x, y: cmp(x[1], y[1])) |
878
|
|
|
return DisplayList(list(items)) |
879
|
|
|
|
880
|
|
|
def getPrefixFor(self, portal_type): |
881
|
|
|
"""Return the prefix for a portal_type. |
882
|
|
|
If not found, simply uses the portal_type itself |
883
|
|
|
""" |
884
|
|
|
prefix = [p for p in self.getIDFormatting() if p['portal_type'] == portal_type] |
885
|
|
|
if prefix: |
886
|
|
|
return prefix[0]['prefix'] |
887
|
|
|
else: |
888
|
|
|
return portal_type |
889
|
|
|
|
890
|
|
|
def getCountries(self): |
891
|
|
|
items = [(x['ISO'], x['Country']) for x in COUNTRIES] |
892
|
|
|
items.sort(lambda x, y: cmp(x[1], y[1])) |
893
|
|
|
return items |
894
|
|
|
|
895
|
|
|
def isRejectionWorkflowEnabled(self): |
896
|
|
|
"""Return true if the rejection workflow is enabled (its checkbox is set) |
897
|
|
|
""" |
898
|
|
|
widget = self.getRejectionReasons() |
899
|
|
|
# widget will be something like: |
900
|
|
|
# [{'checkbox': u'on', 'textfield-2': u'b', 'textfield-1': u'c', 'textfield-0': u'a'}] |
901
|
|
|
if len(widget) > 0: |
902
|
|
|
checkbox = widget[0].get('checkbox', False) |
903
|
|
|
return True if checkbox == 'on' and len(widget[0]) > 1 else False |
904
|
|
|
else: |
905
|
|
|
return False |
906
|
|
|
|
907
|
|
|
def getRejectionReasonsItems(self): |
908
|
|
|
"""Return the list of predefined rejection reasons |
909
|
|
|
""" |
910
|
|
|
reasons = self.getRejectionReasons() |
911
|
|
|
if not reasons: |
912
|
|
|
return [] |
913
|
|
|
reasons = reasons[0] |
914
|
|
|
keys = filter(lambda key: key != "checkbox", reasons.keys()) |
915
|
|
|
return map(lambda key: reasons[key], sorted(keys)) or [] |
916
|
|
|
|
917
|
|
|
def _getNumberOfRequiredVerificationsVocabulary(self): |
918
|
|
|
""" |
919
|
|
|
Returns a DisplayList with the available options for the |
920
|
|
|
multi-verification list: '1', '2', '3', '4' |
921
|
|
|
:returns: DisplayList with the available options for the |
922
|
|
|
multi-verification list |
923
|
|
|
""" |
924
|
|
|
items = [(1, '1'), (2, '2'), (3, '3'), (4, '4')] |
925
|
|
|
return IntDisplayList(list(items)) |
926
|
|
|
|
927
|
|
|
def getIDServerValuesHTML(self): |
928
|
|
|
number_generator = getUtility(INumberGenerator) |
929
|
|
|
keys = number_generator.keys() |
930
|
|
|
values = number_generator.values() |
931
|
|
|
results = [] |
932
|
|
|
for i in range(len(keys)): |
933
|
|
|
results.append('%s: %s' % (keys[i], values[i])) |
934
|
|
|
return "\n".join(results) |
935
|
|
|
|
936
|
|
|
|
937
|
|
|
registerType(BikaSetup, PROJECTNAME) |
938
|
|
|
|