Package Gnumed :: Package business :: Module gmBilling
[frames] | no frames]

Source Code for Module Gnumed.business.gmBilling

  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   
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 #--------------------------------------------------------
70 - def format(self):
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 #--------------------------------------------------------
99 - def _get_is_in_use(self):
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 #------------------------------------------------------------
168 -def delete_billable(pk_billable=None):
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
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 #--------------------------------------------------------
220 - def format(self):
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 #--------------------------------------------------------
278 - def _get_billable(self):
279 return cBillable(aPK_obj = self._payload[self._idx['pk_billable']])
280 281 billable = property(_get_billable, lambda x:x) 282 #--------------------------------------------------------
283 - def _get_bill(self):
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 #--------------------------------------------------------
290 - def _get_is_in_use(self):
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 #------------------------------------------------------------
307 -def create_bill_item(pk_encounter=None, pk_billable=None, pk_staff=None):
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 #------------------------------------------------------------
336 -def delete_bill_item(link_obj=None, pk_bill_item=None):
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
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 #--------------------------------------------------------
378 - def format(self, include_receiver=True, include_doc=True):
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 #--------------------------------------------------------
444 - def add_items(self, items=None):
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 #--------------------------------------------------------
454 - def _get_bill_items(self):
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 #--------------------------------------------------------
459 - def _get_invoice(self):
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 #--------------------------------------------------------
466 - def _get_address(self):
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 #--------------------------------------------------------
475 - def _get_default_address(self):
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 #--------------------------------------------------------
483 - def _get_home_address(self):
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 #------------------------------------------------------------
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 #------------------------------------------------------------
523 -def get_bills4document(pk_document=None):
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 #------------------------------------------------------------
530 -def create_bill(conn=None, invoice_id=None):
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 #------------------------------------------------------------
543 -def delete_bill(link_obj=None, pk_bill=None):
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 #------------------------------------------------------------
550 -def get_bill_receiver(pk_patient=None):
551 pass
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 -def __generate_invoice_id_lock_token(invoice_id):
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 #------------------------------------------------------------
650 -def lock_invoice_id(invoice_id):
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 #------------------------------------------------------------
677 -def unlock_invoice_id(invoice_id):
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 #------------------------------------------------------------
694 -def get_scan2pay_data(branch, bill, provider=None, comment=None):
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 # main 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 # from Gnumed.pycommon import gmLog2 758 # from Gnumed.pycommon import gmI18N 759 from Gnumed.pycommon import gmDateTime 760 # from Gnumed.business import gmPerson 761 from Gnumed.business import gmPraxis 762 763 # gmI18N.activate_locale() 764 gmDateTime.init() 765 766 gmPG2.request_login_params(setup_pool = True) 767 768 #--------------------------------------------------
769 - def test_default_address():
770 bills = get_bills(pk_patient = 12) 771 first_bill = bills[0] 772 print(first_bill.default_address)
773 774 #--------------------------------------------------
775 - def test_me():
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 #me['vat']=4; me.store_payload() 783 784 #--------------------------------------------------
785 - def test_get_scan2pay_data():
786 prax = gmPraxis.get_praxis_branches()[0] 787 bills = get_bills(pk_patient = 12) 788 print(get_scan2pay_data ( 789 prax, 790 bills[0], 791 provider=None, 792 comment = 'GNUmed test harness' + ('x' * 400) 793 ))
794 795 #--------------------------------------------------
796 - def test_generate_invoice_id():
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 #generate_invoice_id(template=None, pk_patient=None, person=None, date_format='%Y-%m-%d', time_format='%H%M%S') 829 830 #-------------------------------------------------- 831 832 #test_me() 833 #test_default_address() 834 #test_get_scan2pay_data() 835 test_generate_invoice_id() 836