1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of SENAITE.HEALTH. |
||
4 | # |
||
5 | # SENAITE.HEALTH is free software: you can redistribute it and/or modify it |
||
6 | # under the terms of the GNU General Public License as published by the Free |
||
7 | # Software Foundation, version 2. |
||
8 | # |
||
9 | # This program is distributed in the hope that it will be useful, but WITHOUT |
||
10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
||
11 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
||
12 | # details. |
||
13 | # |
||
14 | # You should have received a copy of the GNU General Public License along with |
||
15 | # this program; if not, write to the Free Software Foundation, Inc., 51 |
||
16 | # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||
17 | # |
||
18 | # Copyright 2018-2019 by it's authors. |
||
19 | # Some rights reserved, see README and LICENSE. |
||
20 | |||
21 | from datetime import datetime |
||
22 | |||
23 | from Products.ATContentTypes.utils import DT2dt |
||
24 | from Products.ATExtensions.ateapi import RecordsField |
||
25 | from Products.Archetypes import atapi |
||
26 | from Products.Archetypes.public import * |
||
27 | from Products.CMFCore.utils import getToolByName |
||
28 | from Products.CMFPlone.utils import safe_unicode |
||
29 | from zope.interface import implements |
||
30 | |||
31 | from bika.health import bikaMessageFactory as _ |
||
32 | from bika.health import logger |
||
33 | from bika.health.config import * |
||
34 | from bika.health.interfaces import IPatient |
||
35 | from bika.health.utils import translate_i18n as t |
||
36 | from bika.health.widgets import SplittedDateWidget |
||
37 | from bika.health.widgets.patientmenstrualstatuswidget import \ |
||
38 | PatientMenstrualStatusWidget |
||
39 | from bika.lims import api |
||
40 | from bika.lims import idserver |
||
41 | from bika.lims.browser.fields import AddressField |
||
42 | from bika.lims.browser.fields import DateTimeField as DateTimeField_bl |
||
43 | from bika.lims.browser.fields.remarksfield import RemarksField |
||
44 | from bika.lims.browser.widgets import AddressWidget |
||
45 | from bika.lims.browser.widgets import DateTimeWidget as DateTimeWidget_bl |
||
46 | from bika.lims.browser.widgets import RecordsWidget |
||
47 | from bika.lims.browser.widgets import ReferenceWidget |
||
48 | from bika.lims.browser.widgets.remarkswidget import RemarksWidget |
||
49 | from bika.lims.catalog.analysisrequest_catalog import \ |
||
50 | CATALOG_ANALYSIS_REQUEST_LISTING |
||
51 | from bika.lims.catalog.bika_catalog import BIKA_CATALOG |
||
52 | from bika.lims.content.person import Person |
||
53 | from bika.lims.interfaces import IClient |
||
54 | |||
55 | schema = Person.schema.copy() + Schema(( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
56 | StringField( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
57 | 'PatientID', |
||
58 | searchable=1, |
||
59 | required=0, |
||
60 | widget=StringWidget( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
61 | visible={'view': 'visible', 'edit': 'hidden'}, |
||
62 | label=_('Patient ID'), |
||
63 | css='readonly-emphasize', |
||
64 | ), |
||
65 | ), |
||
66 | ReferenceField( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
67 | 'PrimaryReferrer', |
||
68 | allowed_types=('Client',), |
||
69 | relationship='PatientClient', |
||
70 | widget=ReferenceWidget( |
||
71 | label=_("Client"), |
||
72 | size=30, |
||
73 | catalog_name="portal_catalog", |
||
74 | base_query={"is_active": True, |
||
75 | "sort_limit": 30, |
||
76 | "sort_on": "sortable_title", |
||
77 | "sort_order": "ascending"}, |
||
78 | colModel=[ |
||
79 | {"columnName": "Title", "label": _("Title"), |
||
80 | "width": "30", "align": "left"}, |
||
81 | {"columnName": "getProvince", "label": _("Province"), |
||
82 | "width": "30", "align": "left"}, |
||
83 | {"columnName": "getDistrict", "label": _("District"), |
||
84 | "width": "30", "align": "left"}], |
||
85 | showOn=True, |
||
86 | ), |
||
87 | ), |
||
88 | ComputedField( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
89 | 'PrimaryReferrerID', |
||
90 | expression="context.getClientID()", |
||
91 | widget=ComputedWidget( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
92 | ), |
||
93 | ), |
||
94 | ComputedField( |
||
95 | 'PrimaryReferrerTitle', |
||
96 | expression="context.getClientTitle()", |
||
97 | widget=ComputedWidget( |
||
98 | ), |
||
99 | ), |
||
100 | ComputedField( |
||
101 | 'PrimaryReferrerUID', |
||
102 | expression="context.getClientUID()", |
||
103 | widget=ComputedWidget( |
||
104 | ), |
||
105 | ), |
||
106 | ComputedField( |
||
107 | 'PrimaryReferrerURL', |
||
108 | expression="context.getClientURL()", |
||
109 | widget=ComputedWidget( |
||
110 | visible=False |
||
111 | ), |
||
112 | ), |
||
113 | StringField( |
||
114 | 'Gender', |
||
115 | vocabulary=GENDERS, |
||
116 | index='FieldIndex', |
||
117 | default='dk', |
||
118 | widget=SelectionWidget( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
119 | format='select', |
||
120 | label=_('Gender'), |
||
121 | ), |
||
122 | ), |
||
123 | StringField( |
||
124 | 'Age', |
||
125 | widget=StringWidget( |
||
126 | label=_('Age'), |
||
127 | visible=0, |
||
128 | width=3, |
||
129 | ), |
||
130 | ), |
||
131 | DateTimeField_bl( |
||
132 | 'BirthDate', |
||
133 | required=1, |
||
134 | validators=('isDateFormat',), |
||
135 | widget=DateTimeWidget_bl( |
||
136 | label=_('Birth date'), |
||
137 | datepicker_nofuture=1, |
||
138 | ), |
||
139 | ), |
||
140 | BooleanField( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
141 | 'BirthDateEstimated', |
||
142 | default=False, |
||
143 | widget=BooleanWidget( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
144 | label=_('Birth date is estimated'), |
||
145 | ), |
||
146 | ), |
||
147 | RecordsField( |
||
148 | 'AgeSplitted', |
||
149 | required=1, |
||
150 | widget=SplittedDateWidget( |
||
151 | label=_('Age'), |
||
152 | ), |
||
153 | ), |
||
154 | ComputedField( |
||
155 | 'AgeSplittedStr', |
||
156 | expression="context.getAgeSplittedStr()", |
||
157 | widget=ComputedWidget( |
||
158 | visible=False |
||
159 | ), |
||
160 | ), |
||
161 | AddressField( |
||
162 | 'CountryState', |
||
163 | widget=AddressWidget( |
||
164 | searchable=True, |
||
165 | label=_("Country and state"), |
||
166 | showLegend=True, |
||
167 | showDistrict=True, |
||
168 | showCopyFrom=False, |
||
169 | showCity=False, |
||
170 | showPostalCode=False, |
||
171 | showAddress=False, |
||
172 | ), |
||
173 | ), |
||
174 | RecordsField( |
||
175 | 'PatientIdentifiers', |
||
176 | type='patientidentifiers', |
||
177 | subfields=( |
||
178 | 'IdentifierType', |
||
179 | 'Identifier' |
||
180 | ), |
||
181 | subfield_labels={ |
||
182 | 'IdentifierType': _('Identifier Type'), |
||
183 | 'Identifier': _('Identifier') |
||
184 | }, |
||
185 | subfield_sizes={ |
||
186 | 'Identifier': 15, |
||
187 | 'Identifier Type': 25 |
||
188 | }, |
||
189 | widget=RecordsWidget( |
||
190 | label=_('Additional identifiers'), |
||
191 | description=_('Patient additional identifiers'), |
||
192 | combogrid_options={ |
||
193 | 'IdentifierType': { |
||
194 | 'colModel': [ |
||
195 | { |
||
196 | 'columnName': 'IdentifierType', |
||
197 | 'width': '30', |
||
198 | 'label': _('Title') |
||
199 | }, |
||
200 | { |
||
201 | 'columnName': 'Description', |
||
202 | 'width': '70', |
||
203 | 'label': _('Description') |
||
204 | } |
||
205 | ], |
||
206 | 'url': 'getidentifiertypes', |
||
207 | 'showOn': True, |
||
208 | 'width': '550px' |
||
209 | }, |
||
210 | }, |
||
211 | ), |
||
212 | ), |
||
213 | ComputedField( |
||
214 | 'PatientIdentifiersStr', |
||
215 | expression="context.getPatientIdentifiersStr()", |
||
216 | widget=ComputedWidget( |
||
217 | visible=False |
||
218 | ), |
||
219 | ), |
||
220 | RemarksField( |
||
221 | 'Remarks', |
||
222 | searchable=True, |
||
223 | widget=RemarksWidget( |
||
224 | label=_('Remarks'), |
||
225 | ), |
||
226 | ), |
||
227 | RecordsField( |
||
228 | 'TreatmentHistory', |
||
229 | type='treatmenthistory', |
||
230 | subfields=( |
||
231 | 'Treatment', |
||
232 | 'Drug', |
||
233 | 'Start', |
||
234 | 'End' |
||
235 | ), |
||
236 | required_subfields=( |
||
237 | 'Drug', |
||
238 | 'Start', |
||
239 | 'End' |
||
240 | ), |
||
241 | subfield_labels={ |
||
242 | 'Drug': _('Drug'), |
||
243 | 'Start': _('Start'), |
||
244 | 'End': _('End') |
||
245 | }, |
||
246 | subfield_sizes={ |
||
247 | 'Drug': 40, |
||
248 | 'Start': 10, |
||
249 | 'End': 10 |
||
250 | }, |
||
251 | subfield_types={ |
||
252 | 'Start': 'datepicker_nofuture', |
||
253 | 'End': 'datepicker' |
||
254 | }, |
||
255 | widget=RecordsWidget( |
||
256 | label='Drug History', |
||
257 | description=_("A list of patient treatments and drugs administered."), |
||
258 | combogrid_options={ |
||
259 | 'Treatment': { |
||
260 | 'colModel': [ |
||
261 | { |
||
262 | 'columnName': 'Treatment', |
||
263 | 'width': '30', |
||
264 | 'label': _('Title') |
||
265 | }, |
||
266 | { |
||
267 | 'columnName': 'Description', |
||
268 | 'width': '70', |
||
269 | 'label': _('Description') |
||
270 | } |
||
271 | ], |
||
272 | 'url': 'gettreatments', |
||
273 | 'showOn': True, |
||
274 | 'width': '550px' |
||
275 | }, |
||
276 | 'Drug': { |
||
277 | 'colModel': [ |
||
278 | { |
||
279 | 'columnName': 'Drug', |
||
280 | 'width': '30', |
||
281 | 'label': _('Title') |
||
282 | }, |
||
283 | { |
||
284 | 'columnName': 'Description', |
||
285 | 'width': '70', |
||
286 | 'label': _('Description') |
||
287 | } |
||
288 | ], |
||
289 | 'url': 'getdrugs', |
||
290 | 'showOn': True, |
||
291 | 'width': '550px' |
||
292 | }, |
||
293 | }, |
||
294 | ), |
||
295 | ), |
||
296 | RecordsField( |
||
297 | 'Allergies', |
||
298 | type='allergies', |
||
299 | subfields=( |
||
300 | 'DrugProhibition', |
||
301 | 'Drug', |
||
302 | 'Remarks' |
||
303 | ), |
||
304 | required_subfields=( |
||
305 | 'DrugProhibition', |
||
306 | 'Drug' |
||
307 | ), |
||
308 | subfield_labels={ |
||
309 | 'DrugProhibition': _('Drug Prohibition Explanation'), |
||
310 | 'Drug': _('Drug'), |
||
311 | 'Remarks': _('Remarks') |
||
312 | }, |
||
313 | subfield_sizes={ |
||
314 | 'DrugProhibition': 30, |
||
315 | 'Drug': 30, |
||
316 | 'Remarks': 30 |
||
317 | }, |
||
318 | widget=RecordsWidget( |
||
319 | label='Allergies', |
||
320 | description=_("Known Patient allergies to keep information that can aid drug reaction interpretation"), |
||
321 | combogrid_options={ |
||
322 | 'Drug': { |
||
323 | 'colModel': [ |
||
324 | { |
||
325 | 'columnName': 'Title', |
||
326 | 'width': '30', |
||
327 | 'label': _('Title') |
||
328 | }, |
||
329 | { |
||
330 | 'columnName': 'Description', |
||
331 | 'width': '70', |
||
332 | 'label': _('Description') |
||
333 | } |
||
334 | ], |
||
335 | 'url': 'getdrugs', |
||
336 | 'showOn': True, |
||
337 | 'width': '550px' |
||
338 | }, |
||
339 | 'DrugProhibition': { |
||
340 | 'colModel': [ |
||
341 | { |
||
342 | 'columnName': 'DrugProhibition', |
||
343 | 'width': '30', |
||
344 | 'label': _('Title') |
||
345 | }, |
||
346 | { |
||
347 | 'columnName': 'Description', |
||
348 | 'width': '70', |
||
349 | 'label': _('Description') |
||
350 | } |
||
351 | ], |
||
352 | 'url': 'getdrugprohibitions', |
||
353 | 'showOn': True, |
||
354 | 'width': '550px' |
||
355 | }, |
||
356 | }, |
||
357 | ), |
||
358 | ), |
||
359 | RecordsField( |
||
360 | 'ImmunizationHistory', |
||
361 | type='immunizationhistory', |
||
362 | subfields=( |
||
363 | 'EPINumber', |
||
364 | 'Immunization', |
||
365 | 'VaccinationCenter', |
||
366 | 'Date', |
||
367 | 'Remarks' |
||
368 | ), |
||
369 | required_subfields=( |
||
370 | 'EPINumber', |
||
371 | 'Immunization', |
||
372 | 'Date' |
||
373 | ), |
||
374 | subfield_labels={ |
||
375 | 'EPINumber': _('EPI Number'), |
||
376 | 'Immunization': _('Immunization'), |
||
377 | 'VaccinationCenter': _('Vaccination Center'), |
||
378 | 'Date': _('Date'), |
||
379 | 'Remarks': _('Remarks') |
||
380 | }, |
||
381 | subfield_sizes={ |
||
382 | 'EPINumber': 12, |
||
383 | 'Immunization': 20, |
||
384 | 'VaccinationCenter': 10, |
||
385 | 'Date': 10, |
||
386 | 'Remarks': 25 |
||
387 | }, |
||
388 | subfield_types={ |
||
389 | 'Date': 'datepicker_nofuture' |
||
390 | }, |
||
391 | widget=RecordsWidget( |
||
392 | label='Immunization History', |
||
393 | description=_("A list of immunizations administered to the patient."), |
||
394 | combogrid_options={ |
||
395 | 'Immunization': { |
||
396 | 'colModel': [ |
||
397 | { |
||
398 | 'columnName': 'Immunization', |
||
399 | 'width': '30', |
||
400 | 'label': _('Title') |
||
401 | }, |
||
402 | { |
||
403 | 'columnName': 'Description', |
||
404 | 'width': '70', |
||
405 | 'label': _('Description') |
||
406 | } |
||
407 | ], |
||
408 | 'url': 'getimmunizations', |
||
409 | 'showOn': True, |
||
410 | 'width': '550px' |
||
411 | }, |
||
412 | 'VaccinationCenter': { |
||
413 | 'colModel': [ |
||
414 | { |
||
415 | 'columnName': 'VaccinationCenter', |
||
416 | 'width': '100', |
||
417 | 'label': _('Title') |
||
418 | } |
||
419 | ], |
||
420 | 'url': 'getvaccinationcenters', |
||
421 | 'showOn': True, |
||
422 | 'width': '550px' |
||
423 | }, |
||
424 | }, |
||
425 | ), |
||
426 | ), |
||
427 | RecordsField( |
||
428 | 'TravelHistory', |
||
429 | type='travelhistory', |
||
430 | subfields=( |
||
431 | 'TripStartDate', |
||
432 | 'TripEndDate', |
||
433 | 'Country', |
||
434 | 'Location', |
||
435 | 'Remarks' |
||
436 | ), |
||
437 | required_subfields='Country', |
||
438 | subfield_labels={ |
||
439 | 'TripStartDate': _('Trip Start Date', 'Start date'), |
||
440 | 'TripEndDate': _('Trip End Date', 'End date'), |
||
441 | 'Country': _('Country'), |
||
442 | 'Location': _('Location'), |
||
443 | 'Remarks': _('Remarks')}, |
||
444 | subfield_sizes={ |
||
445 | 'TripStartDate': 10, |
||
446 | 'TripEndDate': 10, |
||
447 | 'Country': 20, |
||
448 | 'Location': 20, |
||
449 | 'Remarks': 25}, |
||
450 | subfield_types={ |
||
451 | 'TripStartDate': 'datepicker_nofuture', |
||
452 | 'TripEndDate': 'datepicker' |
||
453 | }, |
||
454 | widget=RecordsWidget( |
||
455 | label='Travel History', |
||
456 | description=_("A list of places visited by the patient."), |
||
457 | combogrid_options={ |
||
458 | 'Country': { |
||
459 | 'colModel': [ |
||
460 | { |
||
461 | 'columnName': 'Code', |
||
462 | 'width': '15', |
||
463 | 'label': _('Code') |
||
464 | }, |
||
465 | { |
||
466 | 'columnName': 'Country', |
||
467 | 'width': '85', |
||
468 | 'label': _('Country') |
||
469 | } |
||
470 | ], |
||
471 | 'url': 'getCountries', |
||
472 | 'showOn': True, |
||
473 | 'width': "450px", |
||
474 | }, |
||
475 | }, |
||
476 | ), |
||
477 | ), |
||
478 | RecordsField( |
||
479 | 'ChronicConditions', |
||
480 | type='chronicconditions', |
||
481 | subfields=( |
||
482 | 'Code', |
||
483 | 'Title', |
||
484 | 'Description', |
||
485 | 'Onset', |
||
486 | 'End' |
||
487 | ), |
||
488 | required_subfields=( |
||
489 | 'Title', |
||
490 | 'Onset' |
||
491 | ), |
||
492 | subfield_sizes={ |
||
493 | 'Code': 7, |
||
494 | 'Title': 20, |
||
495 | 'Description': 35, |
||
496 | 'Onset': 10, |
||
497 | 'End': 10 |
||
498 | }, |
||
499 | subfield_types={ |
||
500 | 'Onset': 'datepicker_nofuture', |
||
501 | 'End': 'datepicker' |
||
502 | }, |
||
503 | widget=RecordsWidget( |
||
504 | label='Past Medical History', |
||
505 | description=_("Patient's past medical history."), |
||
506 | combogrid_options={ |
||
507 | 'Title': { |
||
508 | 'colModel': [ |
||
509 | { |
||
510 | 'columnName': 'Code', |
||
511 | 'width': '10', |
||
512 | 'label': _('Code') |
||
513 | }, |
||
514 | { |
||
515 | 'columnName': 'Title', |
||
516 | 'width': '30', |
||
517 | 'label': _('Title') |
||
518 | }, |
||
519 | { |
||
520 | 'columnName': 'Description', |
||
521 | 'width': '60', |
||
522 | 'label': _('Description') |
||
523 | } |
||
524 | ], |
||
525 | 'url': 'getdiseases', |
||
526 | 'showOn': True, |
||
527 | 'width': "650px", |
||
528 | }, |
||
529 | }, |
||
530 | ), |
||
531 | ), |
||
532 | StringField( |
||
533 | 'BirthPlace', |
||
534 | schemata='Personal', |
||
535 | widget=StringWidget( |
||
536 | label=_('Birth place'), |
||
537 | ), |
||
538 | ), |
||
539 | # TODO This field will be removed on release 319. We maintain this field on release 318 |
||
540 | # because of the transference between string field and content type data. |
||
541 | StringField( |
||
542 | 'Ethnicity', |
||
543 | schemata='Personal', |
||
544 | index='FieldIndex', |
||
545 | vocabulary=ETHNICITIES, |
||
546 | widget=ReferenceWidget( |
||
547 | label=_('Ethnicity'), |
||
548 | description=_("Ethnicity eg. Asian, African, etc."), |
||
549 | visible=False, |
||
550 | ), |
||
551 | ), |
||
552 | # TODO This field will change its name on v319 and it'll be called Ethnicity |
||
553 | ReferenceField( |
||
554 | 'Ethnicity_Obj', |
||
555 | schemata='Personal', |
||
556 | vocabulary='getEthnicitiesVocabulary', |
||
557 | allowed_types=('Ethnicity',), |
||
558 | relationship='PatientEthnicity', |
||
559 | widget=SelectionWidget( |
||
560 | format='select', |
||
561 | label=_('Ethnicity'), |
||
562 | description=_("Ethnicity eg. Asian, African, etc."), |
||
563 | ), |
||
564 | ), |
||
565 | StringField( |
||
566 | 'Citizenship', |
||
567 | schemata='Personal', |
||
568 | widget=StringWidget( |
||
569 | label=_('Citizenship'), |
||
570 | ), |
||
571 | ), |
||
572 | StringField( |
||
573 | 'MothersName', |
||
574 | schemata='Personal', |
||
575 | widget=StringWidget( |
||
576 | label=_('Mothers name'), |
||
577 | ), |
||
578 | ), |
||
579 | StringField( |
||
580 | 'FathersName', |
||
581 | schemata='Personal', |
||
582 | widget=StringWidget( |
||
583 | label=_('Fathers name'), |
||
584 | ), |
||
585 | ), |
||
586 | StringField( |
||
587 | 'CivilStatus', |
||
588 | schemata='Personal', |
||
589 | widget=StringWidget( |
||
590 | label=_('Civil status'), |
||
591 | ), |
||
592 | ), |
||
593 | ImageField( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
594 | 'Photo', |
||
595 | schemata='Identification', |
||
596 | widget=ImageWidget( |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
597 | label=_('Photo'), |
||
598 | ), |
||
599 | ), |
||
600 | ImageField( |
||
601 | 'Feature', |
||
602 | schemata='Identification', |
||
603 | multiValue=1, |
||
604 | widget=ImageWidget( |
||
605 | label=_('Feature'), |
||
606 | ), |
||
607 | ), |
||
608 | RecordsField( |
||
609 | 'MenstrualStatus', |
||
610 | type='menstrualstatus', |
||
611 | widget=PatientMenstrualStatusWidget( |
||
612 | label='Menstrual status', |
||
613 | ), |
||
614 | ), |
||
615 | StringField( |
||
616 | 'ClientPatientID', |
||
617 | searchable=1, |
||
618 | validators=('unique_client_patient_ID_validator',), |
||
619 | required=1, |
||
620 | widget=StringWidget( |
||
621 | label=_('Client Patient ID'), |
||
622 | ), |
||
623 | ), |
||
624 | BooleanField( |
||
625 | 'Anonymous', |
||
626 | default=False, |
||
627 | widget=BooleanWidget( |
||
628 | label=_("Anonymous") |
||
629 | ), |
||
630 | ), |
||
631 | BooleanField( |
||
632 | 'DefaultResultsDistribution', |
||
633 | schemata="Publication preference", |
||
634 | default=True, |
||
635 | widget=BooleanWidget( |
||
636 | label=_("Inherit default settings"), |
||
637 | description=_("If checked, settings will be inherited from " |
||
638 | "the Client, so further changes in Client for this " |
||
639 | "setting will be populated too.")) |
||
640 | ), |
||
641 | BooleanField( |
||
642 | 'AllowResultsDistribution', |
||
643 | schemata="Publication preference", |
||
644 | default=False, |
||
645 | widget=BooleanWidget( |
||
646 | label=_("Allow results distribution to this patient"), |
||
647 | description=_("If checked, results reports will also be sent " |
||
648 | "to the Patient automatically.")) |
||
649 | ), |
||
650 | BooleanField( |
||
651 | 'PublicationAttachmentsPermitted', |
||
652 | default=False, |
||
653 | schemata='Publication preference', |
||
654 | widget=BooleanWidget( |
||
655 | label=_("Results attachments permitted"), |
||
656 | description=_("File attachments to results, e.g. microscope " |
||
657 | "photos, will be included in emails to patient " |
||
658 | "if this option is enabled")) |
||
659 | ), |
||
660 | ReferenceField( |
||
661 | 'InsuranceCompany', |
||
662 | vocabulary='get_insurancecompanies', |
||
663 | allowed_types=('InsuranceCompany',), |
||
664 | relationship='InsuranceCompany', |
||
665 | required=False, |
||
666 | widget=SelectionWidget( |
||
667 | format='select', |
||
668 | label=_('Insurance Company'), |
||
669 | ), |
||
670 | ), |
||
671 | StringField( |
||
672 | 'InsuranceNumber', |
||
673 | searchable=1, |
||
674 | required=0, |
||
675 | widget=StringWidget( |
||
676 | label=_('Insurance Number'), |
||
677 | ), |
||
678 | ), |
||
679 | BooleanField( |
||
680 | 'InvoiceToInsuranceCompany', |
||
681 | default=False, |
||
682 | widget=BooleanWidget( |
||
683 | label=_("Send invoices to the insurance company."), |
||
684 | description=_("If it is checked the invoices will be send to the insurance company." |
||
685 | " In this case the insurance number will be mandatory.")) |
||
686 | ), |
||
687 | BooleanField( |
||
688 | 'PatientAsGuarantor', |
||
689 | schemata='Insurance', |
||
690 | default=True, |
||
691 | widget=BooleanWidget( |
||
692 | label=_("The patient is the guarantor."), |
||
693 | description=_("The patient and the guarantor are the same.")) |
||
694 | ), |
||
695 | StringField( |
||
696 | 'GuarantorID', |
||
697 | searchable=1, |
||
698 | schemata='Insurance', |
||
699 | required=0, |
||
700 | widget=StringWidget( |
||
701 | label=_('Guarantor ID'), |
||
702 | description=_("The ID number (Insurance Number) from the person whose contract cover the current patient.") |
||
703 | ), |
||
704 | ), |
||
705 | StringField( |
||
706 | 'GuarantorSurname', |
||
707 | searchable=1, |
||
708 | schemata='Insurance', |
||
709 | required=0, |
||
710 | widget=StringWidget( |
||
711 | label=_("Guarantor's Surname"), |
||
712 | ), |
||
713 | ), |
||
714 | StringField( |
||
715 | 'GuarantorFirstname', |
||
716 | searchable=1, |
||
717 | schemata='Insurance', |
||
718 | required=0, |
||
719 | widget=StringWidget( |
||
720 | label=_("Guarantor's First Name"), |
||
721 | ), |
||
722 | ), |
||
723 | AddressField( |
||
724 | 'GuarantorPostalAddress', |
||
725 | searchable=1, |
||
726 | schemata='Insurance', |
||
727 | required=0, |
||
728 | widget=AddressWidget( |
||
729 | label=_("Guarantor's postal address"), |
||
730 | ), |
||
731 | ), |
||
732 | StringField( |
||
733 | 'GuarantorBusinessPhone', |
||
734 | schemata='Insurance', |
||
735 | widget=StringWidget( |
||
736 | label=_("Guarantor's Phone (business)"), |
||
737 | ), |
||
738 | ), |
||
739 | StringField( |
||
740 | 'GuarantorHomePhone', |
||
741 | schemata='Insurance', |
||
742 | widget=StringWidget( |
||
743 | label=_("Guarantor's Phone (home)"), |
||
744 | ), |
||
745 | ), |
||
746 | StringField( |
||
747 | 'GuarantorMobilePhone', |
||
748 | schemata='Insurance', |
||
749 | widget=StringWidget( |
||
750 | label=_("Guarantor's Phone (mobile)"), |
||
751 | ), |
||
752 | ), |
||
753 | BooleanField( |
||
754 | 'ConsentSMS', |
||
755 | default=False, |
||
756 | widget=BooleanWidget( |
||
757 | label=_('Consent to SMS'), |
||
758 | ), |
||
759 | ), |
||
760 | ComputedField( |
||
761 | 'NumberOfSamples', |
||
762 | expression="len(context.getSamples())", |
||
763 | widget=ComputedWidget( |
||
764 | visible=False |
||
765 | ), |
||
766 | ), |
||
767 | ComputedField( |
||
768 | 'NumberOfSamplesCancelled', |
||
769 | expression="len(context.getSamplesCancelled())", |
||
770 | widget=ComputedWidget( |
||
771 | visible=False |
||
772 | ), |
||
773 | ), |
||
774 | ComputedField( |
||
775 | 'NumberOfSamplesOngoing', |
||
776 | expression="len(context.getSamplesOngoing())", |
||
777 | widget=ComputedWidget( |
||
778 | visible=False |
||
779 | ), |
||
780 | ), |
||
781 | ComputedField( |
||
782 | 'NumberOfSamplesPublished', |
||
783 | expression="len(context.getSamplesPublished())", |
||
784 | widget=ComputedWidget( |
||
785 | visible=False |
||
786 | ), |
||
787 | ), |
||
788 | ComputedField( |
||
789 | 'RatioOfSamplesOngoing', |
||
790 | expression="context.getNumberOfSamplesOngoingRatio()", |
||
791 | widget=ComputedWidget( |
||
792 | visible=False |
||
793 | ), |
||
794 | ), |
||
795 | )) |
||
796 | |||
797 | schema['JobTitle'].widget.visible = False |
||
798 | schema['Department'].widget.visible = False |
||
799 | schema['BusinessPhone'].widget.visible = False |
||
800 | schema['BusinessFax'].widget.visible = False |
||
801 | # Don't make title required - it will be computed from the Person's Fullname |
||
802 | schema['title'].required = 0 |
||
803 | schema['title'].widget.visible = False |
||
804 | schema['EmailAddress'].schemata = 'Personal' |
||
805 | schema['HomePhone'].schemata = 'Personal' |
||
806 | schema['MobilePhone'].schemata = 'Personal' |
||
807 | schema['InsuranceCompany'].schemata = 'Insurance' |
||
808 | schema['InsuranceNumber'].schemata = 'Insurance' |
||
809 | schema['InvoiceToInsuranceCompany'].schemata = 'Insurance' |
||
810 | schema.moveField('PrimaryReferrer', after='Surname') |
||
811 | schema.moveField('PatientID', before='title') |
||
812 | schema.moveField('ClientPatientID', after='PatientID') |
||
813 | schema.moveField('Anonymous', before='ClientPatientID') |
||
814 | schema.moveField('InsuranceCompany', after='PrimaryReferrer') |
||
815 | schema.moveField('InsuranceNumber', after='InsuranceCompany') |
||
816 | schema.moveField('PatientIdentifiers', after='InsuranceNumber') |
||
817 | schema.moveField('Gender', after='PatientIdentifiers') |
||
818 | schema.moveField('Age', after='Gender') |
||
819 | schema.moveField('BirthDate', after='Age') |
||
820 | schema.moveField('BirthDateEstimated', after='BirthDate') |
||
821 | schema.moveField('AgeSplitted', after='BirthDateEstimated') |
||
822 | schema.moveField('CountryState', after='AgeSplitted') |
||
823 | schema.moveField('MenstrualStatus', after='AgeSplitted') |
||
824 | schema.moveField('ConsentSMS', after='PrimaryReferrer') |
||
825 | schema.moveField('PrimaryReferrer', before='ClientPatientID') |
||
826 | |||
827 | |||
828 | class Patient(Person): |
||
829 | implements(IPatient) |
||
830 | _at_rename_after_creation = True |
||
831 | displayContentsTab = False |
||
832 | schema = schema |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
833 | |||
834 | def _renameAfterCreation(self, check_auto_id=False): |
||
835 | """Autogenerate the ID of the object based on core's ID formatting |
||
836 | settings for this type |
||
837 | """ |
||
838 | idserver.renameAfterCreation(self) |
||
839 | |||
840 | def Title(self): |
||
841 | """Return the Fullname of the patient |
||
842 | """ |
||
843 | return safe_unicode(self.getFullname()).encode('utf-8') |
||
844 | |||
845 | def getPatientID(self): |
||
846 | return self.getId() |
||
847 | |||
848 | def getSamples(self, **kwargs): |
||
849 | """Return samples taken from this Patient |
||
850 | """ |
||
851 | catalog = api.get_tool(CATALOG_ANALYSIS_REQUEST_LISTING, context=self) |
||
852 | query = dict([(k, v) for k, v in kwargs.items() |
||
853 | if k in catalog.indexes()]) |
||
854 | query["getPatientUID"] = api.get_uid(self) |
||
855 | brains = api.search(query, CATALOG_ANALYSIS_REQUEST_LISTING) |
||
856 | if not kwargs.get("full_objects", False): |
||
857 | return brains |
||
858 | return map(api.get_object, brains) |
||
859 | |||
860 | def getSamplesCancelled(self, full_objects=False): |
||
861 | """Return samples taken from this Patient that are in cancelled state |
||
862 | """ |
||
863 | return self.getSamples(review_state="cancelled", |
||
864 | full_objects=full_objects) |
||
865 | |||
866 | def getSamplesPublished(self, full_objects=False): |
||
867 | """Return samples taken from this Patient that are in published state |
||
868 | """ |
||
869 | return self.getSamples(review_state="published", |
||
870 | full_objects=full_objects) |
||
871 | |||
872 | def getSamplesOngoing(self, full_objects=False): |
||
873 | """Return samples taken from this Patient that are ongoing |
||
874 | """ |
||
875 | ongoing_statuses = [ |
||
876 | "to_be_sampled", |
||
877 | "scheduled_sampling", |
||
878 | "to_be_sampled", |
||
879 | "sample_due", |
||
880 | "sample_received", |
||
881 | "attachment_due", |
||
882 | "to_be_verified", |
||
883 | "verified", |
||
884 | "to_be_preserved"] |
||
885 | return self.getSamples(review_state=ongoing_statuses, is_active=True, |
||
886 | full_objects=full_objects) |
||
887 | |||
888 | def getNumberOfSamplesOngoingRatio(self): |
||
889 | """ |
||
890 | Returns the ratio between NumberOfSamplesOngoing/NumberOfSamples |
||
891 | """ |
||
892 | samples = self.getSamples() |
||
893 | if len(samples) > 0: |
||
894 | return len(self.getSamplesOngoing())/len(samples) |
||
895 | return 0 |
||
896 | |||
897 | def get_insurancecompanies(self): |
||
898 | """ |
||
899 | Return all the registered insurance companies. |
||
900 | """ |
||
901 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
902 | # Void selection |
||
903 | ret = [('', '')] |
||
904 | # Other selections |
||
905 | for ic in bsc(portal_type='InsuranceCompany', |
||
906 | is_active=True, |
||
907 | sort_on='sortable_title'): |
||
908 | ret.append((ic.UID, ic.Title)) |
||
909 | return DisplayList(ret) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
910 | |||
911 | def getPatientIdentifiersList(self): |
||
912 | """Returns a list with the additional identifiers for this patient |
||
913 | """ |
||
914 | ids = self.getPatientIdentifiers() |
||
915 | ids = map(lambda patient_id: patient_id.get("Identifier"), ids) |
||
916 | return filter(None, ids) |
||
917 | |||
918 | def getPatientIdentifiersStr(self): |
||
919 | """Returns a string representation of the additional identifiers for |
||
920 | this patient |
||
921 | """ |
||
922 | ids = self.getPatientIdentifiersList() |
||
923 | return " ".join(ids) |
||
924 | |||
925 | def getAgeSplitted(self): |
||
926 | |||
927 | if self.getBirthDate(): |
||
928 | dob = DT2dt(self.getBirthDate()).replace(tzinfo=None) |
||
929 | now = datetime.today() |
||
930 | |||
931 | currentday = now.day |
||
932 | currentmonth = now.month |
||
933 | currentyear = now.year |
||
934 | birthday = dob.day |
||
935 | birthmonth = dob.month |
||
936 | birthyear = dob.year |
||
937 | ageday = currentday - birthday |
||
938 | agemonth = 0 |
||
939 | ageyear = 0 |
||
940 | months31days = [1, 3, 5, 7, 8, 10, 12] |
||
941 | |||
942 | if ageday < 0: |
||
943 | currentmonth -= 1 |
||
944 | if currentmonth < 1: |
||
945 | currentyear -= 1 |
||
946 | currentmonth = currentmonth + 12 |
||
947 | |||
948 | dayspermonth = 30 |
||
949 | if currentmonth in months31days: |
||
950 | dayspermonth = 31 |
||
951 | elif currentmonth == 2: |
||
952 | dayspermonth = 28 |
||
953 | if(currentyear % 4 == 0 |
||
954 | and (currentyear % 100 > 0 or currentyear % 400 == 0)): |
||
955 | dayspermonth += 1 |
||
956 | |||
957 | ageday = ageday + dayspermonth |
||
958 | |||
959 | agemonth = currentmonth - birthmonth |
||
960 | if agemonth < 0: |
||
961 | currentyear -= 1 |
||
962 | agemonth = agemonth + 12 |
||
963 | |||
964 | ageyear = currentyear - birthyear |
||
965 | |||
966 | return [{'year': ageyear, |
||
967 | 'month': agemonth, |
||
968 | 'day': ageday}] |
||
969 | else: |
||
970 | return [{'year': '', |
||
971 | 'month': '', |
||
972 | 'day': ''}] |
||
973 | |||
974 | def getAge(self): |
||
975 | return self.getAgeSplitted()[0]['year'] |
||
976 | |||
977 | def getAgeSplittedStr(self): |
||
978 | splitted = self.getAgeSplitted()[0] |
||
979 | arr = [] |
||
980 | arr.append(splitted['year'] and str(splitted['year']) + 'y' or '') |
||
981 | arr.append(splitted['month'] and str(splitted['month']) + 'm' or '') |
||
982 | arr.append(splitted['day'] and str(splitted['day']) + 'd' or '') |
||
983 | return ' '.join(arr) |
||
984 | |||
985 | def getCountryState(self): |
||
986 | return self.getField('CountryState').get(self) \ |
||
987 | if self.getField('CountryState').get(self) \ |
||
988 | else self.getPhysicalAddress() |
||
989 | |||
990 | def getGuarantorID(self): |
||
991 | """ |
||
992 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
993 | the current patient fields. |
||
994 | :return: The guarantor ID (insurance number) from |
||
995 | """ |
||
996 | return self.getInsuranceNumber() if self.getPatientAsGuarantor() else self.getField('GuarantorID').get(self) |
||
997 | |||
998 | def getGuarantorSurname(self): |
||
999 | """ |
||
1000 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1001 | the current patient fields. |
||
1002 | """ |
||
1003 | return self.getSurname() if self.getPatientAsGuarantor() else self.getField('GuarantorSurname').get(self) |
||
1004 | |||
1005 | def getGuarantorFirstname(self): |
||
1006 | """ |
||
1007 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1008 | the current patient fields. |
||
1009 | """ |
||
1010 | return self.getFirstname() if self.getPatientAsGuarantor() else self.getField('GuarantorFirstname').get(self) |
||
1011 | |||
1012 | def getGuarantorPostalAddress(self): |
||
1013 | """ |
||
1014 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1015 | the current patient fields. |
||
1016 | """ |
||
1017 | return self.getPostalAddress() \ |
||
1018 | if self.getPatientAsGuarantor() \ |
||
1019 | else self.getField('GuarantorPostalAddress').get(self) |
||
1020 | |||
1021 | def getGuarantorBusinessPhone(self): |
||
1022 | """ |
||
1023 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1024 | the current patient fields. |
||
1025 | """ |
||
1026 | return self.getBusinessPhone() \ |
||
1027 | if self.getPatientAsGuarantor() \ |
||
1028 | else self.getField('GuarantorBusinessPhone').get(self) |
||
1029 | |||
1030 | def getGuarantorHomePhone(self): |
||
1031 | """ |
||
1032 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1033 | the current patient fields. |
||
1034 | """ |
||
1035 | return self.getHomePhone() if self.getPatientAsGuarantor() else self.getField('GuarantorHomePhone').get(self) |
||
1036 | |||
1037 | def getGuarantorMobilePhone(self): |
||
1038 | """ |
||
1039 | If the patient is the guarantor, all the fields related with the guarantor are going to have the same value as |
||
1040 | the current patient fields. |
||
1041 | """ |
||
1042 | return self.getMobilePhone() \ |
||
1043 | if self.getPatientAsGuarantor() \ |
||
1044 | else self.getField('GuarantorMobilePhone').get(self) |
||
1045 | |||
1046 | def getEthnicitiesVocabulary(self, instance=None): |
||
1047 | """ |
||
1048 | Obtain all the ethnicities registered in the system and returns them as a list |
||
1049 | """ |
||
1050 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
1051 | items = [(c.UID, c.Title) |
||
1052 | for c in bsc(portal_type='Ethnicity', |
||
1053 | is_active=True)] |
||
1054 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
1055 | items.insert(0, ('', t(_('')))) |
||
1056 | return DisplayList(items) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
1057 | |||
1058 | # TODO This function will will be removed on v319 |
||
1059 | def getEthnicity(self): |
||
1060 | """ |
||
1061 | This function exists because we are changing the construction of ethnicities. Until now, ethnicities options were |
||
1062 | hand-coded but now they are a new content type. So we need to pass all patient's ethnicity values, but to do |
||
1063 | such thing, we need to create new ethnicity types on upgrade step and edit patient ethnicity field to relate them |
||
1064 | with its corresponding ethnicity content type. |
||
1065 | :return: |
||
1066 | """ |
||
1067 | return self.getEthnicity_Obj() |
||
1068 | |||
1069 | # TODO This function will be removed on v319 |
||
1070 | def setEthnicity(self, value): |
||
1071 | self.setEthnicity_Obj(value) |
||
1072 | |||
1073 | def getDocuments(self): |
||
1074 | """ |
||
1075 | Return all the multifile objects related with the patient |
||
1076 | """ |
||
1077 | return self.objectValues('Multifile') |
||
1078 | |||
1079 | def getPrimaryReferrer(self): |
||
1080 | """Returns the client the current Patient is assigned to. Delegates the |
||
1081 | action to function getClient. |
||
1082 | NOTE: This is kept for backwards compatibility |
||
1083 | """ |
||
1084 | logger.warn("Patient.getPrimaryReferrer: better use 'getClient'") |
||
1085 | return self.getClient() |
||
1086 | |||
1087 | def getClient(self): |
||
1088 | """Returns the client the current Patient is assigned to, if any |
||
1089 | """ |
||
1090 | # The schema's field PrimaryReferrer is only used to allow the user to |
||
1091 | # assign the patient to a client in edit form. The entered value is used |
||
1092 | # in ObjectModifiedEventHandler to move the patient to the Client's |
||
1093 | # folder, so the value stored in the Schema's is not used anymore |
||
1094 | # See https://github.com/senaite/senaite.core/pull/152 |
||
1095 | client = self.aq_parent |
||
1096 | if IClient.providedBy(client): |
||
1097 | return client |
||
1098 | return None |
||
1099 | |||
1100 | def setClient(self, value): |
||
1101 | """Sets the client the current Patient has to be assigned to |
||
1102 | """ |
||
1103 | self.setPrimaryReferrer(value) |
||
1104 | |||
1105 | def getClientID(self): |
||
1106 | """Returns the ID of the client this Patient belongs to or None |
||
1107 | """ |
||
1108 | client = self.getClient() |
||
1109 | return client and api.get_id(client) or None |
||
1110 | |||
1111 | def getClientUID(self): |
||
1112 | """Returns the UID of the client this Patient belongs to or None |
||
1113 | """ |
||
1114 | client = self.getClient() |
||
1115 | return client and api.get_uid(client) or None |
||
1116 | |||
1117 | def getClientURL(self): |
||
1118 | """Returns the URL of the client this Patient belongs to or None |
||
1119 | """ |
||
1120 | client = self.getClient() |
||
1121 | return client and api.get_url(client) or None |
||
1122 | |||
1123 | def getClientTitle(self): |
||
1124 | """Returns the title of the client this Patient belongs to or None |
||
1125 | """ |
||
1126 | client = self.getClient() |
||
1127 | return client and api.get_title(client) or None |
||
1128 | |||
1129 | def getBatches(self, full_objects=False): |
||
1130 | """Returns the Batches (Clinic Cases) this Patient is assigned to |
||
1131 | """ |
||
1132 | query = dict(portal_type="Batch", getPatientUID=api.get_uid(self)) |
||
1133 | batches = api.search(query, BIKA_CATALOG) |
||
1134 | if full_objects: |
||
1135 | return map(api.get_object, batches) |
||
1136 | return batches |
||
1137 | |||
1138 | def SearchableText(self): |
||
1139 | """ |
||
1140 | Override searchable text logic based on the requirements. |
||
1141 | |||
1142 | This method constructs a text blob which contains all full-text |
||
1143 | searchable text for this content item. |
||
1144 | https://docs.plone.org/develop/plone/searching_and_indexing/indexing.html#full-text-searching |
||
1145 | """ |
||
1146 | |||
1147 | # Speed up string concatenation ops by using a buffer |
||
1148 | entries = [] |
||
1149 | |||
1150 | # plain text fields we index from ourself, |
||
1151 | # a list of accessor methods of the class |
||
1152 | plain_text_fields = ("Title", "getFullname", "getId", |
||
1153 | "getPrimaryReferrerID", "getPrimaryReferrerTitle", "getClientPatientID") |
||
1154 | |||
1155 | def read(accessor): |
||
1156 | """ |
||
1157 | Call a class accessor method to give a value for certain Archetypes |
||
1158 | field. |
||
1159 | """ |
||
1160 | try: |
||
1161 | value = accessor() |
||
1162 | except: |
||
1163 | value = "" |
||
1164 | |||
1165 | if value is None: |
||
1166 | value = "" |
||
1167 | |||
1168 | return value |
||
1169 | |||
1170 | # Concatenate plain text fields as they are |
||
1171 | for f in plain_text_fields: |
||
1172 | accessor = getattr(self, f) |
||
1173 | value = read(accessor) |
||
1174 | entries.append(value) |
||
1175 | |||
1176 | # Adding HTML Fields to SearchableText can be uncommented if necessary |
||
1177 | # transforms = getToolByName(self, 'portal_transforms') |
||
1178 | # |
||
1179 | # # Run HTML valued fields through text/plain conversion |
||
1180 | # for f in html_fields: |
||
1181 | # accessor = getattr(self, f) |
||
1182 | # value = read(accessor) |
||
1183 | # |
||
1184 | # if value != "": |
||
1185 | # stream = transforms.convertTo('text/plain', value, mimetype='text/html') |
||
1186 | # value = stream.getData() |
||
1187 | # |
||
1188 | # entries.append(value) |
||
1189 | |||
1190 | # Plone accessor methods assume utf-8 |
||
1191 | def convertToUTF8(text): |
||
1192 | if type(text) == unicode: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
1193 | return text.encode("utf-8") |
||
1194 | return text |
||
1195 | |||
1196 | entries = [convertToUTF8(entry) for entry in entries] |
||
1197 | |||
1198 | # Concatenate all strings to one text blob |
||
1199 | return " ".join(entries) |
||
1200 | |||
1201 | |||
1202 | # schemata.finalizeATCTSchema(schema, folderish=True, moveDiscussion=False) |
||
1203 | atapi.registerType(Patient, PROJECTNAME) |
||
1204 |