1
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
29 DEFAULT_INVOICE_ID_TEMPLATE = 'GM%(pk_pat)s / %(date)s / %(time)s'
30
31
32
33
34 _SQL_get_billable_fields = "SELECT * FROM ref.v_billables WHERE %s"
35
36 -class cBillable(gmBusinessDBObject.cBusinessDBObject):
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
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
107 -def get_billables(active_only=True, order_by=None, return_pks=False):
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
126 -def create_billable(code=None, term=None, data_source=None, return_existing=False):
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
183
184 _SQL_get_bill_item_fields = u"SELECT * FROM bill.v_bill_items WHERE %s"
185
186 -class cBillItem(gmBusinessDBObject.cBusinessDBObject):
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
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
291 return self._payload[self._idx['pk_bill']] is not None
292
293 is_in_use = property(_get_is_in_use, lambda x:x)
294
295 -def get_bill_items(pk_patient=None, non_invoiced_only=False, return_pks=False):
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
343
344 _SQL_get_bill_fields = """SELECT * FROM bill.v_bills WHERE %s"""
345
346 -class cBill(gmBusinessDBObject.cBusinessDBObject):
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
443
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
472
473 address = property(_get_address, lambda x:x)
474
480
481 default_address = property(_get_default_address, lambda x:x)
482
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
503 -def get_bills(order_by=None, pk_patient=None, return_pks=False):
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
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
625
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
633 """Turn invoice ID into integer token for PG level locking.
634
635 CAUTION: This is NOT compatible with any of 1.8 or below.
636 Do NOT attempt to run this against a v22 database at the
637 risk of duplicate invoice IDs.
638 """
639 _log.debug('invoice ID: %s', invoice_id)
640 data4adler32 = 'adler32---[%s]---[%s]' % (invoice_id, invoice_id)
641 adler32 = zlib.adler32(bytes(data4adler32, 'utf8'))
642 _log.debug('adler32: %s', adler32)
643 data4crc32 = 'crc32---[%s]---[adler32:%s]' % (invoice_id, adler32)
644 _log.debug('data for CRC 32: %s', data4crc32)
645 crc32 = zlib.crc32(bytes(data4crc32, 'utf8'), adler32)
646 _log.debug('CRC 32: %s', crc32)
647 return crc32
648
649
651 """Lock an invoice ID.
652
653 The lock taken is an exclusive advisory lock in PostgreSQL.
654
655 Because the data is short _and_ crc32/adler32 are fairly
656 weak we assume that collisions can be created "easily".
657 Therefore we apply both algorithms concurrently.
658
659 NOT compatible with anything 1.8 or below.
660 """
661 _log.debug('locking invoice ID: %s', invoice_id)
662 token = __generate_invoice_id_lock_token(invoice_id)
663 cmd = "SELECT pg_try_advisory_lock(%s)" % token
664 try:
665 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
666 except gmPG2.dbapi.ProgrammingError:
667 _log.exception('cannot lock invoice ID: [%s] (%s)', invoice_id, token)
668 return False
669
670 if rows[0][0]:
671 return True
672
673 _log.error('cannot lock invoice ID: [%s] (%s)', invoice_id, token)
674 return False
675
676
678 _log.debug('unlocking invoice ID: %s', invoice_id)
679 token = __generate_invoice_id_lock_token(invoice_id)
680 cmd = u"SELECT pg_advisory_unlock(%s)" % token
681 try:
682 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
683 except gmPG2.dbapi.ProgrammingError:
684 _log.exception('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
685 return False
686
687 if rows[0][0]:
688 return True
689
690 _log.error('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
691 return False
692
693
695 """Create scan2pay data for generating a QR code.
696
697 https://www.scan2pay.info
698 --------------------------------
699 BCD # (3) fixed, barcode tag
700 002 # (3) fixed, version
701 1 # (1) charset, 1 = utf8
702 SCT # (3) fixed
703 $<praxis_id::BIC//Bank//%(value)s::11>$ # (11) <BIC>
704 $2<range_of::$<current_provider_name::%(lastnames)s::>$,$<praxis::%(praxis)s::>$::70>2$ # (70) <Name of beneficiary> "Empfänger" - Praxis
705 $<praxis_id::IBAN//Bank//%(value)s::34>$ # (34) <IBAN>
706 EUR$<bill::%(total_amount_with_vat)s::12>$ # (12) <Amount in EURO> "EUR12.5"
707 # (4) <purpose of transfer> - leer
708 # (35) <remittance info - struct> - only this XOR the next field - GNUmed: leer
709 $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"
710 <beneficiary-to-payor info> # (70) "pay soon :-)" - optional - GNUmed nur wenn bytes verfügbar
711 --------------------------------
712 total: 331 bytes (not chars ! - cave UTF8)
713 EOL: LF or CRLF
714 last *used* element not followed by anything, IOW can omit pending non-used elements
715 """
716 assert (branch is not None), '<branch> must not be <None>'
717 assert (bill is not None), '<bill> must not be <None>'
718
719 data = {}
720 IBANs = branch.get_external_ids(id_type = 'IBAN', issuer = 'Bank')
721 if len(IBANs) == 0:
722 _log.debug('no IBAN found, cannot create scan2pay data')
723 return None
724 data['IBAN'] = IBANs[0]['value'][:34]
725 data['beneficiary'] = gmTools.coalesce (
726 value2test = provider,
727 return_instead = branch['praxis'][:70],
728 template4value = '%%(lastnames)s, %s' % branch['praxis']
729 )[:70]
730 BICs = branch.get_external_ids(id_type = 'BIC', issuer = 'Bank')
731 if len(BICs) == 0:
732 data['BIC'] = ''
733 else:
734 data['BIC'] = BICs[0]['value'][:11]
735 data['amount'] = bill['total_amount_with_vat'][:9]
736 data['ref'] = (_('Inv: %s, %s') % (
737 bill['invoice_id'],
738 gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%d.%B %Y')
739 ))[:140]
740 data['cmt'] = gmTools.coalesce(comment, '', '\n%s')[:70]
741
742 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
743 data_str_bytes = bytes(data_str, 'utf8')[:331]
744 return str(data_str_bytes, 'utf8')
745
746
747
748
749 if __name__ == "__main__":
750
751 if len(sys.argv) < 2:
752 sys.exit()
753
754 if sys.argv[1] != 'test':
755 sys.exit()
756
757
758
759 from Gnumed.pycommon import gmDateTime
760
761 from Gnumed.business import gmPraxis
762
763
764 gmDateTime.init()
765
766 gmPG2.request_login_params(setup_pool = True)
767
768
773
774
776 print("--------------")
777 me = cBillable(aPK_obj=1)
778 fields = me.get_fields()
779 for field in fields:
780 print(field, ':', me[field])
781 print("updatable:", me.get_updatable_fields())
782
783
784
794
795
797 from Gnumed.pycommon import gmI18N
798 gmI18N.activate_locale()
799 gmI18N.install_domain()
800 import gmPerson
801 for idx in range(1,15):
802 print ('')
803 print ('classic:', generate_invoice_id(pk_patient = idx))
804 pat = gmPerson.cPerson(idx)
805 template = u'%(firstname).4s%(lastname).4s%(date)s'
806 print ('new: template = "%s" => %s' % (
807 template,
808 generate_invoice_id (
809 template = template,
810 pk_patient = None,
811 person = pat,
812 date_format='%d%m%Y',
813 time_format='%H%M%S'
814 )
815 ))
816 template = u'%(firstname).4s%(lastname).4s%(date)s-#counter#'
817 new_id = generate_invoice_id (
818 template = template,
819 pk_patient = None,
820 person = pat,
821 date_format='%d%m%Y',
822 time_format='%H%M%S'
823 )
824 print('locked: %s' % lock_invoice_id(new_id))
825 print ('new: template = "%s" => %s' % (template, new_id))
826 print('unlocked: %s' % unlock_invoice_id(new_id))
827
828
829
830
831
832
833
834
835 test_generate_invoice_id()
836