| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2 """Billing code.
3
4 Copyright: authors
5 """
6 #============================================================
7 __author__ = "Nico Latzer <nl@mnet-online.de>, Karsten Hilbert <Karsten.Hilbert@gmx.net>"
8 __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
9
10 import sys
11 import logging
12 import zlib
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmPG2
18 from Gnumed.pycommon import gmBusinessDBObject
19 from Gnumed.pycommon import gmTools
20 from Gnumed.pycommon import gmDateTime
21
22 from Gnumed.business import gmDemographicRecord
23 from Gnumed.business import gmDocuments
24
25 _log = logging.getLogger('gm.bill')
26
27 INVOICE_DOCUMENT_TYPE = 'invoice'
28 # default: old style
29 DEFAULT_INVOICE_ID_TEMPLATE = 'GM%(pk_pat)s / %(date)s / %(time)s'
30
31 #============================================================
32 # billables
33 #------------------------------------------------------------
34 _SQL_get_billable_fields = "SELECT * FROM ref.v_billables WHERE %s"
35
37 """Items which can be billed to patients."""
38
39 _cmd_fetch_payload = _SQL_get_billable_fields % "pk_billable = %s"
40 _cmds_store_payload = [
41 """UPDATE ref.billable SET
42 fk_data_source = %(pk_data_source)s,
43 code = %(billable_code)s,
44 term = %(billable_description)s,
45 comment = gm.nullify_empty_string(%(comment)s),
46 amount = %(raw_amount)s,
47 currency = %(currency)s,
48 vat_multiplier = %(vat_multiplier)s,
49 active = %(active)s
50 --, discountable = %(discountable)s
51 WHERE
52 pk = %(pk_billable)s
53 AND
54 xmin = %(xmin_billable)s
55 RETURNING
56 xmin AS xmin_billable
57 """]
58
59 _updatable_fields = [
60 'billable_code',
61 'billable_description',
62 'raw_amount',
63 'vat_multiplier',
64 'comment',
65 'currency',
66 'active',
67 'pk_data_source'
68 ]
69 #--------------------------------------------------------
71 txt = '%s [#%s]\n\n' % (
72 gmTools.bool2subst (
73 self._payload[self._idx['active']],
74 _('Active billable item'),
75 _('Inactive billable item')
76 ),
77 self._payload[self._idx['pk_billable']]
78 )
79 txt += ' %s: %s\n' % (
80 self._payload[self._idx['billable_code']],
81 self._payload[self._idx['billable_description']]
82 )
83 txt += _(' %(curr)s%(raw_val)s + %(perc_vat)s%% VAT = %(curr)s%(val_w_vat)s\n') % {
84 'curr': self._payload[self._idx['currency']],
85 'raw_val': self._payload[self._idx['raw_amount']],
86 'perc_vat': self._payload[self._idx['vat_multiplier']] * 100,
87 'val_w_vat': self._payload[self._idx['amount_with_vat']]
88 }
89 txt += ' %s %s%s (%s)' % (
90 self._payload[self._idx['catalog_short']],
91 self._payload[self._idx['catalog_version']],
92 gmTools.coalesce(self._payload[self._idx['catalog_language']], '', ' - %s'),
93 self._payload[self._idx['catalog_long']]
94 )
95 txt += gmTools.coalesce(self._payload[self._idx['comment']], '', '\n %s')
96
97 return txt
98 #--------------------------------------------------------
100 cmd = 'SELECT EXISTS(SELECT 1 FROM bill.bill_item WHERE fk_billable = %(pk)s LIMIT 1)'
101 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self._payload[self._idx['pk_billable']]}}])
102 return rows[0][0]
103
104 is_in_use = property(_get_is_in_use, lambda x:x)
105
106 #------------------------------------------------------------
108
109 if order_by is None:
110 order_by = ' ORDER BY catalog_long, catalog_version, billable_code'
111 else:
112 order_by = ' ORDER BY %s' % order_by
113
114 if active_only:
115 where = 'active IS true'
116 else:
117 where = 'true'
118
119 cmd = (_SQL_get_billable_fields % where) + order_by
120 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
121 if return_pks:
122 return [ r['pk_billable'] for r in rows ]
123 return [ cBillable(row = {'data': r, 'idx': idx, 'pk_field': 'pk_billable'}) for r in rows ]
124
125 #------------------------------------------------------------
127 args = {
128 'code': code.strip(),
129 'term': term.strip(),
130 'data_src': data_source
131 }
132 cmd = """
133 INSERT INTO ref.billable (code, term, fk_data_source)
134 SELECT
135 %(code)s,
136 %(term)s,
137 %(data_src)s
138 WHERE NOT EXISTS (
139 SELECT 1 FROM ref.billable
140 WHERE
141 code = %(code)s
142 AND
143 term = %(term)s
144 AND
145 fk_data_source = %(data_src)s
146 )
147 RETURNING pk"""
148 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False, return_data = True)
149 if len(rows) > 0:
150 return cBillable(aPK_obj = rows[0]['pk'])
151
152 if not return_existing:
153 return None
154
155 cmd = """
156 SELECT * FROM ref.v_billables
157 WHERE
158 code = %(code)s
159 AND
160 term = %(term)s
161 AND
162 pk_data_source = %(data_src)s
163 """
164 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
165 return cBillable(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_billable'})
166
167 #------------------------------------------------------------
169 cmd = """
170 DELETE FROM ref.billable
171 WHERE
172 pk = %(pk)s
173 AND
174 NOT EXISTS (
175 SELECT 1 FROM bill.bill_item WHERE fk_billable = %(pk)s
176 )
177 """
178 args = {'pk': pk_billable}
179 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
180
181 #============================================================
182 # bill items
183 #------------------------------------------------------------
184 _SQL_get_bill_item_fields = u"SELECT * FROM bill.v_bill_items WHERE %s"
185
187
188 _cmd_fetch_payload = _SQL_get_bill_item_fields % u"pk_bill_item = %s"
189 _cmds_store_payload = [
190 """UPDATE bill.bill_item SET
191 fk_provider = %(pk_provider)s,
192 fk_encounter = %(pk_encounter_to_bill)s,
193 date_to_bill = %(raw_date_to_bill)s,
194 description = gm.nullify_empty_string(%(item_detail)s),
195 net_amount_per_unit = %(net_amount_per_unit)s,
196 currency = gm.nullify_empty_string(%(currency)s),
197 fk_bill = %(pk_bill)s,
198 unit_count = %(unit_count)s,
199 amount_multiplier = %(amount_multiplier)s
200 WHERE
201 pk = %(pk_bill_item)s
202 AND
203 xmin = %(xmin_bill_item)s
204 RETURNING
205 xmin AS xmin_bill_item
206 """]
207
208 _updatable_fields = [
209 'pk_provider',
210 'pk_encounter_to_bill',
211 'raw_date_to_bill',
212 'item_detail',
213 'net_amount_per_unit',
214 'currency',
215 'pk_bill',
216 'unit_count',
217 'amount_multiplier'
218 ]
219 #--------------------------------------------------------
221 txt = '%s (%s %s%s) [#%s]\n' % (
222 gmTools.bool2subst(
223 self._payload[self._idx['pk_bill']] is None,
224 _('Open item'),
225 _('Billed item'),
226 ),
227 self._payload[self._idx['catalog_short']],
228 self._payload[self._idx['catalog_version']],
229 gmTools.coalesce(self._payload[self._idx['catalog_language']], '', ' - %s'),
230 self._payload[self._idx['pk_bill_item']]
231 )
232 txt += ' %s: %s\n' % (
233 self._payload[self._idx['billable_code']],
234 self._payload[self._idx['billable_description']]
235 )
236 txt += gmTools.coalesce (
237 self._payload[self._idx['billable_comment']],
238 '',
239 ' (%s)\n',
240 )
241 txt += gmTools.coalesce (
242 self._payload[self._idx['item_detail']],
243 '',
244 _(' Details: %s\n'),
245 )
246
247 txt += '\n'
248 txt += _(' %s of units: %s\n') % (
249 gmTools.u_numero,
250 self._payload[self._idx['unit_count']]
251 )
252 txt += _(' Amount per unit: %(curr)s%(val_p_unit)s (%(cat_curr)s%(cat_val)s per catalog)\n') % {
253 'curr': self._payload[self._idx['currency']],
254 'val_p_unit': self._payload[self._idx['net_amount_per_unit']],
255 'cat_curr': self._payload[self._idx['billable_currency']],
256 'cat_val': self._payload[self._idx['billable_amount']]
257 }
258 txt += _(' Amount multiplier: %s\n') % self._payload[self._idx['amount_multiplier']]
259 txt += _(' VAT would be: %(perc_vat)s%% %(equals)s %(curr)s%(vat)s\n') % {
260 'perc_vat': self._payload[self._idx['vat_multiplier']] * 100,
261 'equals': gmTools.u_corresponds_to,
262 'curr': self._payload[self._idx['currency']],
263 'vat': self._payload[self._idx['vat']]
264 }
265
266 txt += '\n'
267 txt += _(' Charge date: %s') % gmDateTime.pydt_strftime (
268 self._payload[self._idx['date_to_bill']],
269 '%Y %b %d',
270 accuracy = gmDateTime.acc_days
271 )
272 bill = self.bill
273 if bill is not None:
274 txt += _('\n On bill: %s') % bill['invoice_id']
275
276 return txt
277 #--------------------------------------------------------
279 return cBillable(aPK_obj = self._payload[self._idx['pk_billable']])
280
281 billable = property(_get_billable, lambda x:x)
282 #--------------------------------------------------------
284 if self._payload[self._idx['pk_bill']] is None:
285 return None
286 return cBill(aPK_obj = self._payload[self._idx['pk_bill']])
287
288 bill = property(_get_bill, lambda x:x)
289 #--------------------------------------------------------
292
293 is_in_use = property(_get_is_in_use, lambda x:x)
294 #------------------------------------------------------------
296 if non_invoiced_only:
297 cmd = _SQL_get_bill_item_fields % u"pk_patient = %(pat)s AND pk_bill IS NULL"
298 else:
299 cmd = _SQL_get_bill_item_fields % u"pk_patient = %(pat)s"
300 args = {'pat': pk_patient}
301 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
302 if return_pks:
303 return [ r['pk_bill_item'] for r in rows ]
304 return [ cBillItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill_item'}) for r in rows ]
305
306 #------------------------------------------------------------
308
309 billable = cBillable(aPK_obj = pk_billable)
310 cmd = """
311 INSERT INTO bill.bill_item (
312 fk_provider,
313 fk_encounter,
314 net_amount_per_unit,
315 currency,
316 fk_billable
317 ) VALUES (
318 %(staff)s,
319 %(enc)s,
320 %(val)s,
321 %(curr)s,
322 %(billable)s
323 )
324 RETURNING pk"""
325 args = {
326 'staff': pk_staff,
327 'enc': pk_encounter,
328 'val': billable['raw_amount'],
329 'curr': billable['currency'],
330 'billable': pk_billable
331 }
332 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
333 return cBillItem(aPK_obj = rows[0][0])
334
335 #------------------------------------------------------------
337 cmd = 'DELETE FROM bill.bill_item WHERE pk = %(pk)s AND fk_bill IS NULL'
338 args = {'pk': pk_bill_item}
339 gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
340
341 #============================================================
342 # bills
343 #------------------------------------------------------------
344 _SQL_get_bill_fields = """SELECT * FROM bill.v_bills WHERE %s"""
345
347 """Represents a bill"""
348
349 _cmd_fetch_payload = _SQL_get_bill_fields % "pk_bill = %s"
350 _cmds_store_payload = [
351 """UPDATE bill.bill SET
352 invoice_id = gm.nullify_empty_string(%(invoice_id)s),
353 close_date = %(close_date)s,
354 apply_vat = %(apply_vat)s,
355 comment = gm.nullify_empty_string(%(comment)s),
356 fk_receiver_identity = %(pk_receiver_identity)s,
357 fk_receiver_address = %(pk_receiver_address)s,
358 fk_doc = %(pk_doc)s
359 WHERE
360 pk = %(pk_bill)s
361 AND
362 xmin = %(xmin_bill)s
363 RETURNING
364 pk as pk_bill,
365 xmin as xmin_bill
366 """
367 ]
368 _updatable_fields = [
369 'invoice_id',
370 'pk_receiver_identity',
371 'close_date',
372 'apply_vat',
373 'comment',
374 'pk_receiver_address',
375 'pk_doc'
376 ]
377 #--------------------------------------------------------
379 txt = '%s [#%s]\n' % (
380 gmTools.bool2subst (
381 (self._payload[self._idx['close_date']] is None),
382 _('Open bill'),
383 _('Closed bill')
384 ),
385 self._payload[self._idx['pk_bill']]
386 )
387 txt += _(' Invoice ID: %s\n') % self._payload[self._idx['invoice_id']]
388
389 if self._payload[self._idx['close_date']] is not None:
390 txt += _(' Closed: %s\n') % gmDateTime.pydt_strftime (
391 self._payload[self._idx['close_date']],
392 '%Y %b %d',
393 accuracy = gmDateTime.acc_days
394 )
395
396 if self._payload[self._idx['comment']] is not None:
397 txt += _(' Comment: %s\n') % self._payload[self._idx['comment']]
398
399 txt += _(' Bill value: %(curr)s%(val)s\n') % {
400 'curr': self._payload[self._idx['currency']],
401 'val': self._payload[self._idx['total_amount']]
402 }
403
404 if self._payload[self._idx['apply_vat']] is None:
405 txt += _(' VAT: undecided\n')
406 elif self._payload[self._idx['apply_vat']] is True:
407 txt += _(' VAT: %(perc_vat)s%% %(equals)s %(curr)s%(vat)s\n') % {
408 'perc_vat': self._payload[self._idx['percent_vat']],
409 'equals': gmTools.u_corresponds_to,
410 'curr': self._payload[self._idx['currency']],
411 'vat': self._payload[self._idx['total_vat']]
412 }
413 txt += _(' Value + VAT: %(curr)s%(val)s\n') % {
414 'curr': self._payload[self._idx['currency']],
415 'val': self._payload[self._idx['total_amount_with_vat']]
416 }
417 else:
418 txt += _(' VAT: does not apply\n')
419
420 if self._payload[self._idx['pk_bill_items']] is None:
421 txt += _(' Items billed: 0\n')
422 else:
423 txt += _(' Items billed: %s\n') % len(self._payload[self._idx['pk_bill_items']])
424 if include_doc:
425 txt += _(' Invoice: %s\n') % (
426 gmTools.bool2subst (
427 self._payload[self._idx['pk_doc']] is None,
428 _('not available'),
429 '#%s' % self._payload[self._idx['pk_doc']]
430 )
431 )
432 txt += _(' Patient: #%s\n') % self._payload[self._idx['pk_patient']]
433 if include_receiver:
434 txt += gmTools.coalesce (
435 self._payload[self._idx['pk_receiver_identity']],
436 '',
437 _(' Receiver: #%s\n')
438 )
439 if self._payload[self._idx['pk_receiver_address']] is not None:
440 txt += '\n '.join(gmDemographicRecord.get_patient_address(pk_patient_address = self._payload[self._idx['pk_receiver_address']]).format())
441
442 return txt
443 #--------------------------------------------------------
445 """Requires no pending changes within the bill itself."""
446 # should check for item consistency first
447 conn = gmPG2.get_connection(readonly = False)
448 for item in items:
449 item['pk_bill'] = self._payload[self._idx['pk_bill']]
450 item.save(conn = conn)
451 conn.commit()
452 self.refetch_payload() # make sure aggregates are re-filled from view
453 #--------------------------------------------------------
455 return [ cBillItem(aPK_obj = pk) for pk in self._payload[self._idx['pk_bill_items']] ]
456
457 bill_items = property(_get_bill_items, lambda x:x)
458 #--------------------------------------------------------
460 if self._payload[self._idx['pk_doc']] is None:
461 return None
462 return gmDocuments.cDocument(aPK_obj = self._payload[self._idx['pk_doc']])
463
464 invoice = property(_get_invoice, lambda x:x)
465 #--------------------------------------------------------
467 if self._payload[self._idx['pk_receiver_address']] is None:
468 return None
469 return gmDemographicRecord.get_address_from_patient_address_pk (
470 pk_patient_address = self._payload[self._idx['pk_receiver_address']]
471 )
472
473 address = property(_get_address, lambda x:x)
474 #--------------------------------------------------------
476 return gmDemographicRecord.get_patient_address_by_type (
477 pk_patient = self._payload[self._idx['pk_patient']],
478 adr_type = 'billing'
479 )
480
481 default_address = property(_get_default_address, lambda x:x)
482 #--------------------------------------------------------
484 return gmDemographicRecord.get_patient_address_by_type (
485 pk_patient = self._payload[self._idx['pk_patient']],
486 adr_type = 'home'
487 )
488
489 home_address = property(_get_home_address, lambda x:x)
490 #--------------------------------------------------------
492 if self._payload[self._idx['pk_receiver_address']] is not None:
493 return True
494 adr = self.default_address
495 if adr is None:
496 adr = self.home_address
497 if adr is None:
498 return False
499 self['pk_receiver_address'] = adr['pk_lnk_person_org_address']
500 return self.save_payload()
501
502 #------------------------------------------------------------
504
505 args = {'pat': pk_patient}
506 where_parts = ['true']
507
508 if pk_patient is not None:
509 where_parts.append('pk_patient = %(pat)s')
510
511 if order_by is None:
512 order_by = ''
513 else:
514 order_by = ' ORDER BY %s' % order_by
515
516 cmd = (_SQL_get_bill_fields % ' AND '.join(where_parts)) + order_by
517 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
518 if return_pks:
519 return [ r['pk_bill'] for r in rows ]
520 return [ cBill(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill'}) for r in rows ]
521
522 #------------------------------------------------------------
524 args = {'pk_doc': pk_document}
525 cmd = _SQL_get_bill_fields % 'pk_doc = %(pk_doc)s'
526 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
527 return [ cBill(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill'}) for r in rows ]
528
529 #------------------------------------------------------------
531
532 args = {'inv_id': invoice_id}
533 cmd = """
534 INSERT INTO bill.bill (invoice_id)
535 VALUES (gm.nullify_empty_string(%(inv_id)s))
536 RETURNING pk
537 """
538 rows, idx = gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
539
540 return cBill(aPK_obj = rows[0]['pk'])
541
542 #------------------------------------------------------------
544 args = {'pk': pk_bill}
545 cmd = "DELETE FROM bill.bill WHERE pk = %(pk)s"
546 gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
547 return True
548
549 #------------------------------------------------------------
552
553 #------------------------------------------------------------
554 -def generate_invoice_id(template=None, pk_patient=None, person=None, date_format='%Y-%m-%d', time_format='%H%M%S'):
555 """Generate invoice ID string, based on template.
556
557 No template given -> generate old style fixed format invoice ID.
558
559 Placeholders:
560 %(pk_pat)s
561 %(date)s
562 %(time)s
563 if included, $counter$ is not *needed* (but still possible)
564 %(firstname)s
565 %(lastname)s
566 %(dob)s
567
568 #counter#
569 will be replaced by a counter, counting up from 1 until the invoice id is unique, max 999999
570 """
571 assert (None in [pk_patient, person]), u'either of <pk_patient> or <person> can be defined, but not both'
572
573 if (template is None) or (template.strip() == u''):
574 template = DEFAULT_INVOICE_ID_TEMPLATE
575 date_format = '%Y-%m-%d'
576 time_format = '%H%M%S'
577 template = template.strip()
578 _log.debug('invoice ID template: %s', template)
579 if pk_patient is None:
580 if person is not None:
581 pk_patient = person.ID
582 now = gmDateTime.pydt_now_here()
583 data = {}
584 data['pk_pat'] = gmTools.coalesce(pk_patient, '?')
585 data['date'] = gmDateTime.pydt_strftime(now, date_format).strip()
586 data['time'] = gmDateTime.pydt_strftime(now, time_format).strip()
587 if person is None:
588 data['firstname'] = u'?'
589 data['lastname'] = u'?'
590 data['dob'] = u'?'
591 else:
592 data['firstname'] = person['firstnames'].replace(' ', gmTools.u_space_as_open_box).strip()
593 data['lastname'] = person['lastnames'].replace(' ', gmTools.u_space_as_open_box).strip()
594 data['dob'] = person.get_formatted_dob (
595 format = date_format,
596 none_string = u'?',
597 honor_estimation = False
598 ).strip()
599 candidate_invoice_id = template % data
600 if u'#counter#' not in candidate_invoice_id:
601 if u'%(time)s' in template:
602 return candidate_invoice_id
603
604 candidate_invoice_id = candidate_invoice_id + u' [##counter#]'
605
606 _log.debug('invoice id candidate: %s', candidate_invoice_id)
607 # get existing invoice IDs consistent with candidate
608 search_term = u'^\s*%s\s*$' % gmPG2.sanitize_pg_regex(expression = candidate_invoice_id).replace(u'#counter#', '\d+')
609 cmd = u'SELECT invoice_id FROM bill.bill WHERE invoice_id ~* %(search_term)s UNION ALL SELECT invoice_id FROM audit.log_bill WHERE invoice_id ~* %(search_term)s'
610 args = {'search_term': search_term}
611 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
612 if len(rows) == 0:
613 return candidate_invoice_id.replace(u'#counter#', u'1')
614
615 existing_invoice_ids = [ r['invoice_id'].strip() for r in rows ]
616 counter = None
617 counter_max = 999999
618 for idx in range(1, counter_max):
619 candidate = candidate_invoice_id.replace(u'#counter#', '%s' % idx)
620 if candidate not in existing_invoice_ids:
621 counter = idx
622 break
623 if counter is None:
624 # exhausted the range, unlikely (1 million bills are possible
625 # even w/o any other invoice ID data) but technically possible
626 _log.debug('exhausted uniqueness space of [%s] invoice IDs per template', counter_max)
627 counter = '>%s[%s]' % (counter_max, data['time'])
628
629 return candidate_invoice_id.replace(u'#counter#', '%s' % counter)
630
631 #------------------------------------------------------------
632 #------------------------------------------------------------
633 # Remaining problems with invoice ID locking:
634 #
635 # If you run a 1.8.0rc1 client the lock can overflow PG's pg_try_advisory_lock().
636 #
637 # If you run both 1.8.0rc1 and 1.7 (<1.7.9) and happen to lock at the same
638 # time the lock may succeed when it should not, because crc32/adler32 return
639 # different representationel ranges in py2 and py3.
640 #
641 # If you run 1.7 (<1.7.9) on both "Python < 2.6" and "Python 2.6 or beyond"
642 # and happen to lock at the same time the lock may succeed when it should
643 # not, because crc32/adler32 return results with signedness depending on
644 # platform.
645 #------------------------------------------------------------
646 #
647 # remove in 1.9 / DB v23:
649 """Get 1.7 legacy (<1.7.9) lock.
650
651 The fix is to *down*shift py3 checksums into the py2 result range.
652 How to do that was suggested by MRAB on the Python mailing list.
653
654 https://www.mail-archive.com/python-list@python.org/msg447989.html
655
656 Problems:
657 - on py2 < 2.6 (client 1.7) signedness inconsistent across platforms
658 - on py3 (client 1.8), range shifted by & 0xffffffff
659
660 Because both 1.7 (<1.7.9) and 1.8 can run against the same
661 database v22 we need to retain this legacy lock until DB v23.
662 """
663 _log.debug('legacy locking invoice ID: %s', invoice_id)
664 py3_crc32 = zlib.crc32(bytes(invoice_id, 'utf8'))
665 py3_adler32 = zlib.adler32(bytes(invoice_id, 'utf8'))
666 signed_crc32 = py3_crc32 - (py3_crc32 & 0x80000000) * 2
667 signed_adler32 = py3_adler32 - (py3_adler32 & 0x80000000) * 2
668 _log.debug('crc32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_crc32, signed_crc32)
669 _log.debug('adler32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_adler32, signed_adler32)
670 cmd = u"""SELECT pg_try_advisory_lock(%s, %s)""" % (signed_crc32, signed_adler32)
671 try:
672 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
673 except gmPG2.dbapi.ProgrammingError:
674 # should not happen
675 _log.exception('cannot lock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
676 return False
677
678 if rows[0][0]:
679 return True
680
681 _log.error('cannot lock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
682 return False
683
684 #------------------------------------------------------------
686 """Lock an invoice ID.
687
688 The lock taken is an exclusive advisory lock in PostgreSQL.
689
690 Because the data is short _and_ crc32/adler32 are fairly
691 weak we assume that collisions can be created "easily".
692 Therefore we apply both algorithms concurrently.
693 """
694 _log.debug('locking invoice ID: %s', invoice_id)
695 # remove in 1.9 / DB v23:
696 if not __lock_invoice_id_1_7_legacy(invoice_id):
697 return False
698
699 # get py2/py3 compatible lock:
700 # - upshift py2 result by & 0xffffffff for signedness consistency
701 # - still use both crc32 and adler32 but chain the result of
702 # the former into the latter so we can take advantage of
703 # pg_try_advisory_lock(BIGINT)
704 unsigned_crc32 = zlib.crc32(bytes(invoice_id, 'utf8')) & 0xffffffff
705 _log.debug('unsigned crc32: %s', unsigned_crc32)
706 data4adler32 = u'%s---[%s]' % (invoice_id, unsigned_crc32)
707 _log.debug('data for adler32: %s', data4adler32)
708 unsigned_adler32 = zlib.adler32(bytes(data4adler32, 'utf8'), unsigned_crc32) & 0xffffffff
709 _log.debug('unsigned (crc32-chained) adler32: %s', unsigned_adler32)
710 cmd = u"SELECT pg_try_advisory_lock(%s)" % (unsigned_adler32)
711 try:
712 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
713 except gmPG2.dbapi.ProgrammingError:
714 _log.exception('cannot lock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
715 return False
716
717 if rows[0][0]:
718 return True
719
720 _log.error('cannot lock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
721 return False
722
723 #------------------------------------------------------------
725 _log.debug('legacy unlocking invoice ID: %s', invoice_id)
726 py3_crc32 = zlib.crc32(bytes(invoice_id, 'utf8'))
727 py3_adler32 = zlib.adler32(bytes(invoice_id, 'utf8'))
728 signed_crc32 = py3_crc32 - (py3_crc32 & 0x80000000) * 2
729 signed_adler32 = py3_adler32 - (py3_adler32 & 0x80000000) * 2
730 _log.debug('crc32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_crc32, signed_crc32)
731 _log.debug('adler32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_adler32, signed_adler32)
732 cmd = u"""SELECT pg_advisory_unlock(%s, %s)""" % (signed_crc32, signed_adler32)
733 try:
734 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
735 except gmPG2.dbapi.ProgrammingError:
736 _log.exception('cannot unlock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
737 return False
738
739 if rows[0][0]:
740 return True
741
742 _log.error('cannot unlock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
743 return False
744
745 #------------------------------------------------------------
747 _log.debug('unlocking invoice ID: %s', invoice_id)
748 # remove in 1.9 / DB v23:
749 if not __unlock_invoice_id_1_7_legacy(invoice_id):
750 return False
751
752 # unlock
753 unsigned_crc32 = zlib.crc32(bytes(invoice_id, 'utf8')) & 0xffffffff
754 _log.debug('unsigned crc32: %s', unsigned_crc32)
755 data4adler32 = u'%s---[%s]' % (invoice_id, unsigned_crc32)
756 _log.debug('data for adler32: %s', data4adler32)
757 unsigned_adler32 = zlib.adler32(bytes(data4adler32, 'utf8'), unsigned_crc32) & 0xffffffff
758 _log.debug('unsigned (crc32-chained) adler32: %s', unsigned_adler32)
759 cmd = u"SELECT pg_advisory_unlock(%s)" % (unsigned_adler32)
760 try:
761 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
762 except gmPG2.dbapi.ProgrammingError:
763 _log.exception('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
764 return False
765
766 if rows[0][0]:
767 return True
768
769 _log.error('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
770 return False
771
772 #------------------------------------------------------------
774 """Create scan2pay data for generating a QR code.
775
776 https://www.scan2pay.info
777 --------------------------------
778 BCD # (3) fixed, barcode tag
779 002 # (3) fixed, version
780 1 # (1) charset, 1 = utf8
781 SCT # (3) fixed
782 $<praxis_id::BIC//Bank//%(value)s::11>$ # (11) <BIC>
783 $2<range_of::$<current_provider_name::%(lastnames)s::>$,$<praxis::%(praxis)s::>$::70>2$ # (70) <Name of beneficiary> "Empfänger" - Praxis
784 $<praxis_id::IBAN//Bank//%(value)s::34>$ # (34) <IBAN>
785 EUR$<bill::%(total_amount_with_vat)s::12>$ # (12) <Amount in EURO> "EUR12.5"
786 # (4) <purpose of transfer> - leer
787 # (35) <remittance info - struct> - only this XOR the next field - GNUmed: leer
788 $2<range_of::InvID=$<bill::%(invoice_id)s::>$/Date=$<today::%d.%B %Y::>$::140$>2$ # (140) <remittance info - text> "Client:Marie Louise La Lune" - "Rg Nr, date"
789 <beneficiary-to-payor info> # (70) "pay soon :-)" - optional - GNUmed nur wenn bytes verfügbar
790 --------------------------------
791 total: 331 bytes (not chars ! - cave UTF8)
792 EOL: LF or CRLF
793 last *used* element not followed by anything, IOW can omit pending non-used elements
794 """
795 assert (branch is not None), '<branch> must not be <None>'
796 assert (bill is not None), '<bill> must not be <None>'
797
798 data = {}
799 IBANs = branch.get_external_ids(id_type = 'IBAN', issuer = 'Bank')
800 if len(IBANs) == 0:
801 _log.debug('no IBAN found, cannot create scan2pay data')
802 return None
803 data['IBAN'] = IBANs[0]['value'][:34]
804 data['beneficiary'] = gmTools.coalesce (
805 value2test = provider,
806 return_instead = branch['praxis'][:70],
807 template4value = '%%(lastnames)s, %s' % branch['praxis']
808 )[:70]
809 BICs = branch.get_external_ids(id_type = 'BIC', issuer = 'Bank')
810 if len(BICs) == 0:
811 data['BIC'] = ''
812 else:
813 data['BIC'] = BICs[0]['value'][:11]
814 data['amount'] = bill['total_amount_with_vat'][:9]
815 data['ref'] = (_('Inv: %s, %s') % (
816 bill['invoice_id'],
817 gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%d.%B %Y')
818 ))[:140]
819 data['cmt'] = gmTools.coalesce(comment, '', '\n%s')[:70]
820
821 data_str = 'BCD\n002\n1\nSCT\n%(BIC)s\n%(beneficiary)s\n%(IBAN)s\nEUR%(amount)s\n\n\n%(ref)s%(cmt)s' % data
822 data_str_bytes = bytes(data_str, 'utf8')[:331]
823 return str(data_str_bytes, 'utf8')
824
825 #============================================================
826 # main
827 #------------------------------------------------------------
828 if __name__ == "__main__":
829
830 if len(sys.argv) < 2:
831 sys.exit()
832
833 if sys.argv[1] != 'test':
834 sys.exit()
835
836 # from Gnumed.pycommon import gmLog2
837 # from Gnumed.pycommon import gmI18N
838 # from Gnumed.business import gmPerson
839 from Gnumed.business import gmPraxis
840
841 # gmI18N.activate_locale()
842 gmDateTime.init()
843
845 bills = get_bills(pk_patient = 12)
846 first_bill = bills[0]
847 print(first_bill.default_address)
848
849 #--------------------------------------------------
851 print("--------------")
852 me = cBillable(aPK_obj=1)
853 fields = me.get_fields()
854 for field in fields:
855 print(field, ':', me[field])
856 print("updatable:", me.get_updatable_fields())
857 #me['vat']=4; me.store_payload()
858
859 #--------------------------------------------------
861 prax = gmPraxis.get_praxis_branches()[0]
862 bills = get_bills(pk_patient = 12)
863 print(get_scan2pay_data (
864 prax,
865 bills[0],
866 provider=None,
867 comment = 'GNUmed test harness' + ('x' * 400)
868 ))
869
870 #--------------------------------------------------
872 from Gnumed.pycommon import gmI18N
873 gmI18N.activate_locale()
874 gmI18N.install_domain()
875 import gmPerson
876 for idx in range(1,15):
877 print ('')
878 print ('classic:', generate_invoice_id(pk_patient = idx))
879 pat = gmPerson.cPerson(idx)
880 template = u'%(firstname).4s%(lastname).4s%(date)s'
881 print ('new: template = "%s" => %s' % (
882 template,
883 generate_invoice_id (
884 template = template,
885 pk_patient = None,
886 person = pat,
887 date_format='%d%m%Y',
888 time_format='%H%M%S'
889 )
890 ))
891 template = u'%(firstname).4s%(lastname).4s%(date)s-#counter#'
892 new_id = generate_invoice_id (
893 template = template,
894 pk_patient = None,
895 person = pat,
896 date_format='%d%m%Y',
897 time_format='%H%M%S'
898 )
899 print('locked: %s' % lock_invoice_id(new_id))
900 print('new: template = "%s" => %s' % (
901 template,
902 new_id
903 ))
904 print('unlocked: %s' % unlock_invoice_id(new_id))
905
906 #generate_invoice_id(template=None, pk_patient=None, person=None, date_format='%Y-%m-%d', time_format='%H%M%S')
907
908 #--------------------------------------------------
909
910 #test_me()
911 #test_default_address()
912 #test_get_scan2pay_data()
913 test_generate_invoice_id()
914
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sat Feb 29 02:55:27 2020 | http://epydoc.sourceforge.net |