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   
 13   
 14  if __name__ == '__main__': 
 15          sys.path.insert(0, '../../') 
 16  from Gnumed.pycommon import gmPG2 
 17  from Gnumed.pycommon import gmBusinessDBObject 
 18  from Gnumed.pycommon import gmTools 
 19  from Gnumed.pycommon import gmDateTime 
 20  from Gnumed.business import gmDemographicRecord 
 21  from Gnumed.business import gmDocuments 
 22   
 23  _log = logging.getLogger('gm.bill') 
 24   
 25  INVOICE_DOCUMENT_TYPE = 'invoice' 
 26  #============================================================ 
 27  # billables 
 28  #------------------------------------------------------------ 
 29  _SQL_get_billable_fields = "SELECT * FROM ref.v_billables WHERE %s" 
 30   
31 -class cBillable(gmBusinessDBObject.cBusinessDBObject):
32 """Items which can be billed to patients.""" 33 34 _cmd_fetch_payload = _SQL_get_billable_fields % "pk_billable = %s" 35 _cmds_store_payload = [ 36 """UPDATE ref.billable SET 37 fk_data_source = %(pk_data_source)s, 38 code = %(billable_code)s, 39 term = %(billable_description)s, 40 comment = gm.nullify_empty_string(%(comment)s), 41 amount = %(raw_amount)s, 42 currency = %(currency)s, 43 vat_multiplier = %(vat_multiplier)s, 44 active = %(active)s 45 --, discountable = %(discountable)s 46 WHERE 47 pk = %(pk_billable)s 48 AND 49 xmin = %(xmin_billable)s 50 RETURNING 51 xmin AS xmin_billable 52 """] 53 54 _updatable_fields = [ 55 'billable_code', 56 'billable_description', 57 'raw_amount', 58 'vat_multiplier', 59 'comment', 60 'currency', 61 'active', 62 'pk_data_source' 63 ] 64 #--------------------------------------------------------
65 - def format(self):
66 txt = '%s [#%s]\n\n' % ( 67 gmTools.bool2subst ( 68 self._payload[self._idx['active']], 69 _('Active billable item'), 70 _('Inactive billable item') 71 ), 72 self._payload[self._idx['pk_billable']] 73 ) 74 txt += ' %s: %s\n' % ( 75 self._payload[self._idx['billable_code']], 76 self._payload[self._idx['billable_description']] 77 ) 78 txt += _(' %(curr)s%(raw_val)s + %(perc_vat)s%% VAT = %(curr)s%(val_w_vat)s\n') % { 79 'curr': self._payload[self._idx['currency']], 80 'raw_val': self._payload[self._idx['raw_amount']], 81 'perc_vat': self._payload[self._idx['vat_multiplier']] * 100, 82 'val_w_vat': self._payload[self._idx['amount_with_vat']] 83 } 84 txt += ' %s %s%s (%s)' % ( 85 self._payload[self._idx['catalog_short']], 86 self._payload[self._idx['catalog_version']], 87 gmTools.coalesce(self._payload[self._idx['catalog_language']], '', ' - %s'), 88 self._payload[self._idx['catalog_long']] 89 ) 90 txt += gmTools.coalesce(self._payload[self._idx['comment']], '', '\n %s') 91 92 return txt
93 #--------------------------------------------------------
94 - def _get_is_in_use(self):
95 cmd = 'SELECT EXISTS(SELECT 1 FROM bill.bill_item WHERE fk_billable = %(pk)s LIMIT 1)' 96 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self._payload[self._idx['pk_billable']]}}]) 97 return rows[0][0]
98 99 is_in_use = property(_get_is_in_use, lambda x:x)
100 101 #------------------------------------------------------------
102 -def get_billables(active_only=True, order_by=None):
103 104 if order_by is None: 105 order_by = ' ORDER BY catalog_long, catalog_version, billable_code' 106 else: 107 order_by = ' ORDER BY %s' % order_by 108 109 if active_only: 110 where = 'active IS true' 111 else: 112 where = 'true' 113 114 cmd = (_SQL_get_billable_fields % where) + order_by 115 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True) 116 return [ cBillable(row = {'data': r, 'idx': idx, 'pk_field': 'pk_billable'}) for r in rows ]
117 118 #------------------------------------------------------------
119 -def create_billable(code=None, term=None, data_source=None, return_existing=False):
120 args = { 121 'code': code.strip(), 122 'term': term.strip(), 123 'data_src': data_source 124 } 125 cmd = """ 126 INSERT INTO ref.billable (code, term, fk_data_source) 127 SELECT 128 %(code)s, 129 %(term)s, 130 %(data_src)s 131 WHERE NOT EXISTS ( 132 SELECT 1 FROM ref.billable 133 WHERE 134 code = %(code)s 135 AND 136 term = %(term)s 137 AND 138 fk_data_source = %(data_src)s 139 ) 140 RETURNING pk""" 141 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False, return_data = True) 142 if len(rows) > 0: 143 return cBillable(aPK_obj = rows[0]['pk']) 144 145 if not return_existing: 146 return None 147 148 cmd = """ 149 SELECT * FROM ref.v_billables 150 WHERE 151 code = %(code)s 152 AND 153 term = %(term)s 154 AND 155 pk_data_source = %(data_src)s 156 """ 157 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 158 return cBillable(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_billable'})
159 160 #------------------------------------------------------------
161 -def delete_billable(pk_billable=None):
162 cmd = """ 163 DELETE FROM ref.billable 164 WHERE 165 pk = %(pk)s 166 AND 167 NOT EXISTS ( 168 SELECT 1 FROM bill.bill_item WHERE fk_billable = %(pk)s 169 ) 170 """ 171 args = {'pk': pk_billable} 172 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
173 174 #============================================================ 175 # bill items 176 #------------------------------------------------------------ 177 _SQL_fetch_bill_item_fields = "SELECT * FROM bill.v_bill_items WHERE %s" 178
179 -class cBillItem(gmBusinessDBObject.cBusinessDBObject):
180 181 _cmd_fetch_payload = _SQL_fetch_bill_item_fields % "pk_bill_item = %s" 182 _cmds_store_payload = [ 183 """UPDATE bill.bill_item SET 184 fk_provider = %(pk_provider)s, 185 fk_encounter = %(pk_encounter_to_bill)s, 186 date_to_bill = %(raw_date_to_bill)s, 187 description = gm.nullify_empty_string(%(item_detail)s), 188 net_amount_per_unit = %(net_amount_per_unit)s, 189 currency = gm.nullify_empty_string(%(currency)s), 190 fk_bill = %(pk_bill)s, 191 unit_count = %(unit_count)s, 192 amount_multiplier = %(amount_multiplier)s 193 WHERE 194 pk = %(pk_bill_item)s 195 AND 196 xmin = %(xmin_bill_item)s 197 RETURNING 198 xmin AS xmin_bill_item 199 """] 200 201 _updatable_fields = [ 202 'pk_provider', 203 'pk_encounter_to_bill', 204 'raw_date_to_bill', 205 'item_detail', 206 'net_amount_per_unit', 207 'currency', 208 'pk_bill', 209 'unit_count', 210 'amount_multiplier' 211 ] 212 #--------------------------------------------------------
213 - def format(self):
214 txt = '%s (%s %s%s) [#%s]\n' % ( 215 gmTools.bool2subst( 216 self._payload[self._idx['pk_bill']] is None, 217 _('Open item'), 218 _('Billed item'), 219 ), 220 self._payload[self._idx['catalog_short']], 221 self._payload[self._idx['catalog_version']], 222 gmTools.coalesce(self._payload[self._idx['catalog_language']], '', ' - %s'), 223 self._payload[self._idx['pk_bill_item']] 224 ) 225 txt += ' %s: %s\n' % ( 226 self._payload[self._idx['billable_code']], 227 self._payload[self._idx['billable_description']] 228 ) 229 txt += gmTools.coalesce ( 230 self._payload[self._idx['billable_comment']], 231 '', 232 ' (%s)\n', 233 ) 234 txt += gmTools.coalesce ( 235 self._payload[self._idx['item_detail']], 236 '', 237 _(' Details: %s\n'), 238 ) 239 240 txt += '\n' 241 txt += _(' %s of units: %s\n') % ( 242 gmTools.u_numero, 243 self._payload[self._idx['unit_count']] 244 ) 245 txt += _(' Amount per unit: %(curr)s%(val_p_unit)s (%(cat_curr)s%(cat_val)s per catalog)\n') % { 246 'curr': self._payload[self._idx['currency']], 247 'val_p_unit': self._payload[self._idx['net_amount_per_unit']], 248 'cat_curr': self._payload[self._idx['billable_currency']], 249 'cat_val': self._payload[self._idx['billable_amount']] 250 } 251 txt += _(' Amount multiplier: %s\n') % self._payload[self._idx['amount_multiplier']] 252 txt += _(' VAT would be: %(perc_vat)s%% %(equals)s %(curr)s%(vat)s\n') % { 253 'perc_vat': self._payload[self._idx['vat_multiplier']] * 100, 254 'equals': gmTools.u_corresponds_to, 255 'curr': self._payload[self._idx['currency']], 256 'vat': self._payload[self._idx['vat']] 257 } 258 259 txt += '\n' 260 txt += _(' Charge date: %s') % gmDateTime.pydt_strftime ( 261 self._payload[self._idx['date_to_bill']], 262 '%Y %b %d', 263 accuracy = gmDateTime.acc_days 264 ) 265 bill = self.bill 266 if bill is not None: 267 txt += _('\n On bill: %s') % bill['invoice_id'] 268 269 return txt
270 #--------------------------------------------------------
271 - def _get_billable(self):
272 return cBillable(aPK_obj = self._payload[self._idx['pk_billable']])
273 274 billable = property(_get_billable, lambda x:x) 275 #--------------------------------------------------------
276 - def _get_bill(self):
277 if self._payload[self._idx['pk_bill']] is None: 278 return None 279 return cBill(aPK_obj = self._payload[self._idx['pk_bill']])
280 281 bill = property(_get_bill, lambda x:x) 282 #--------------------------------------------------------
283 - def _get_is_in_use(self):
284 return self._payload[self._idx['pk_bill']] is not None
285 286 is_in_use = property(_get_is_in_use, lambda x:x)
287 #------------------------------------------------------------
288 -def get_bill_items(pk_patient=None, non_invoiced_only=False):
289 if non_invoiced_only: 290 cmd = _SQL_fetch_bill_item_fields % "pk_patient = %(pat)s AND pk_bill IS NULL" 291 else: 292 cmd = _SQL_fetch_bill_item_fields % "pk_patient = %(pat)s" 293 args = {'pat': pk_patient} 294 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 295 return [ cBillItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill_item'}) for r in rows ]
296 297 #------------------------------------------------------------
298 -def create_bill_item(pk_encounter=None, pk_billable=None, pk_staff=None):
299 300 billable = cBillable(aPK_obj = pk_billable) 301 cmd = """ 302 INSERT INTO bill.bill_item ( 303 fk_provider, 304 fk_encounter, 305 net_amount_per_unit, 306 currency, 307 fk_billable 308 ) VALUES ( 309 %(staff)s, 310 %(enc)s, 311 %(val)s, 312 %(curr)s, 313 %(billable)s 314 ) 315 RETURNING pk""" 316 args = { 317 'staff': pk_staff, 318 'enc': pk_encounter, 319 'val': billable['raw_amount'], 320 'curr': billable['currency'], 321 'billable': pk_billable 322 } 323 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True) 324 return cBillItem(aPK_obj = rows[0][0])
325 326 #------------------------------------------------------------
327 -def delete_bill_item(link_obj=None, pk_bill_item=None):
328 cmd = 'DELETE FROM bill.bill_item WHERE pk = %(pk)s AND fk_bill IS NULL' 329 args = {'pk': pk_bill_item} 330 gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
331 332 #============================================================ 333 # bills 334 #------------------------------------------------------------ 335 _SQL_get_bill_fields = """SELECT * FROM bill.v_bills WHERE %s""" 336
337 -class cBill(gmBusinessDBObject.cBusinessDBObject):
338 """Represents a bill""" 339 340 _cmd_fetch_payload = _SQL_get_bill_fields % "pk_bill = %s" 341 _cmds_store_payload = [ 342 """UPDATE bill.bill SET 343 invoice_id = gm.nullify_empty_string(%(invoice_id)s), 344 close_date = %(close_date)s, 345 apply_vat = %(apply_vat)s, 346 comment = gm.nullify_empty_string(%(comment)s), 347 fk_receiver_identity = %(pk_receiver_identity)s, 348 fk_receiver_address = %(pk_receiver_address)s, 349 fk_doc = %(pk_doc)s 350 WHERE 351 pk = %(pk_bill)s 352 AND 353 xmin = %(xmin_bill)s 354 RETURNING 355 pk as pk_bill, 356 xmin as xmin_bill 357 """ 358 ] 359 _updatable_fields = [ 360 'invoice_id', 361 'pk_receiver_identity', 362 'close_date', 363 'apply_vat', 364 'comment', 365 'pk_receiver_address', 366 'pk_doc' 367 ] 368 #--------------------------------------------------------
369 - def format(self, include_receiver=True, include_doc=True):
370 txt = '%s [#%s]\n' % ( 371 gmTools.bool2subst ( 372 (self._payload[self._idx['close_date']] is None), 373 _('Open bill'), 374 _('Closed bill') 375 ), 376 self._payload[self._idx['pk_bill']] 377 ) 378 txt += _(' Invoice ID: %s\n') % self._payload[self._idx['invoice_id']] 379 380 if self._payload[self._idx['close_date']] is not None: 381 txt += _(' Closed: %s\n') % gmDateTime.pydt_strftime ( 382 self._payload[self._idx['close_date']], 383 '%Y %b %d', 384 accuracy = gmDateTime.acc_days 385 ) 386 387 if self._payload[self._idx['comment']] is not None: 388 txt += _(' Comment: %s\n') % self._payload[self._idx['comment']] 389 390 txt += _(' Bill value: %(curr)s%(val)s\n') % { 391 'curr': self._payload[self._idx['currency']], 392 'val': self._payload[self._idx['total_amount']] 393 } 394 395 if self._payload[self._idx['apply_vat']] is None: 396 txt += _(' VAT: undecided\n') 397 elif self._payload[self._idx['apply_vat']] is True: 398 txt += _(' VAT: %(perc_vat)s%% %(equals)s %(curr)s%(vat)s\n') % { 399 'perc_vat': self._payload[self._idx['percent_vat']], 400 'equals': gmTools.u_corresponds_to, 401 'curr': self._payload[self._idx['currency']], 402 'vat': self._payload[self._idx['total_vat']] 403 } 404 txt += _(' Value + VAT: %(curr)s%(val)s\n') % { 405 'curr': self._payload[self._idx['currency']], 406 'val': self._payload[self._idx['total_amount_with_vat']] 407 } 408 else: 409 txt += _(' VAT: does not apply\n') 410 411 if self._payload[self._idx['pk_bill_items']] is None: 412 txt += _(' Items billed: 0\n') 413 else: 414 txt += _(' Items billed: %s\n') % len(self._payload[self._idx['pk_bill_items']]) 415 if include_doc: 416 txt += _(' Invoice: %s\n') % ( 417 gmTools.bool2subst ( 418 self._payload[self._idx['pk_doc']] is None, 419 _('not available'), 420 '#%s' % self._payload[self._idx['pk_doc']] 421 ) 422 ) 423 txt += _(' Patient: #%s\n') % self._payload[self._idx['pk_patient']] 424 if include_receiver: 425 txt += gmTools.coalesce ( 426 self._payload[self._idx['pk_receiver_identity']], 427 '', 428 _(' Receiver: #%s\n') 429 ) 430 if self._payload[self._idx['pk_receiver_address']] is not None: 431 txt += '\n '.join(gmDemographicRecord.get_patient_address(pk_patient_address = self._payload[self._idx['pk_receiver_address']]).format()) 432 433 return txt
434 #--------------------------------------------------------
435 - def add_items(self, items=None):
436 """Requires no pending changes within the bill itself.""" 437 # should check for item consistency first 438 conn = gmPG2.get_connection(readonly = False) 439 for item in items: 440 item['pk_bill'] = self._payload[self._idx['pk_bill']] 441 item.save(conn = conn) 442 conn.commit() 443 self.refetch_payload() # make sure aggregates are re-filled from view
444 #--------------------------------------------------------
445 - def _get_bill_items(self):
446 return [ cBillItem(aPK_obj = pk) for pk in self._payload[self._idx['pk_bill_items']] ]
447 448 bill_items = property(_get_bill_items, lambda x:x) 449 #--------------------------------------------------------
450 - def _get_invoice(self):
451 if self._payload[self._idx['pk_doc']] is None: 452 return None 453 return gmDocuments.cDocument(aPK_obj = self._payload[self._idx['pk_doc']])
454 455 invoice = property(_get_invoice, lambda x:x) 456 #--------------------------------------------------------
457 - def _get_address(self):
458 if self._payload[self._idx['pk_receiver_address']] is None: 459 return None 460 return gmDemographicRecord.get_address_from_patient_address_pk ( 461 pk_patient_address = self._payload[self._idx['pk_receiver_address']] 462 )
463 464 address = property(_get_address, lambda x:x) 465 #--------------------------------------------------------
466 - def _get_default_address(self):
467 return gmDemographicRecord.get_patient_address_by_type ( 468 pk_patient = self._payload[self._idx['pk_patient']], 469 adr_type = 'billing' 470 )
471 472 default_address = property(_get_default_address, lambda x:x) 473 #--------------------------------------------------------
474 - def _get_home_address(self):
475 return gmDemographicRecord.get_patient_address_by_type ( 476 pk_patient = self._payload[self._idx['pk_patient']], 477 adr_type = 'home' 478 )
479 480 home_address = property(_get_home_address, lambda x:x) 481 #--------------------------------------------------------
483 if self._payload[self._idx['pk_receiver_address']] is not None: 484 return True 485 adr = self.default_address 486 if adr is None: 487 adr = self.home_address 488 if adr is None: 489 return False 490 self['pk_receiver_address'] = adr['pk_lnk_person_org_address'] 491 return self.save_payload()
492 493 #------------------------------------------------------------
494 -def get_bills(order_by=None, pk_patient=None):
495 496 args = {'pat': pk_patient} 497 where_parts = ['true'] 498 499 if pk_patient is not None: 500 where_parts.append('pk_patient = %(pat)s') 501 502 if order_by is None: 503 order_by = '' 504 else: 505 order_by = ' ORDER BY %s' % order_by 506 507 cmd = (_SQL_get_bill_fields % ' AND '.join(where_parts)) + order_by 508 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 509 return [ cBill(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill'}) for r in rows ]
510 511 #------------------------------------------------------------
512 -def get_bills4document(pk_document=None):
513 args = {'pk_doc': pk_document} 514 cmd = _SQL_get_bill_fields % 'pk_doc = %(pk_doc)s' 515 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 516 return [ cBill(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill'}) for r in rows ]
517 518 #------------------------------------------------------------
519 -def create_bill(conn=None, invoice_id=None):
520 521 args = {'inv_id': invoice_id} 522 cmd = """ 523 INSERT INTO bill.bill (invoice_id) 524 VALUES (gm.nullify_empty_string(%(inv_id)s)) 525 RETURNING pk 526 """ 527 rows, idx = gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False) 528 529 return cBill(aPK_obj = rows[0]['pk'])
530 531 #------------------------------------------------------------
532 -def delete_bill(link_obj=None, pk_bill=None):
533 args = {'pk': pk_bill} 534 cmd = "DELETE FROM bill.bill WHERE pk = %(pk)s" 535 gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}]) 536 return True
537 538 #------------------------------------------------------------
539 -def get_bill_receiver(pk_patient=None):
540 pass
541 542 #------------------------------------------------------------
543 -def get_invoice_id(pk_patient=None):
544 return 'GM%s / %s' % ( 545 pk_patient, 546 gmDateTime.pydt_strftime ( 547 gmDateTime.pydt_now_here(), 548 '%Y-%m-%d / %H%M%S' 549 ) 550 )
551 552 #============================================================ 553 # main 554 #------------------------------------------------------------ 555 if __name__ == "__main__": 556 557 if len(sys.argv) < 2: 558 sys.exit() 559 560 if sys.argv[1] != 'test': 561 sys.exit() 562 563 # from Gnumed.pycommon import gmLog2 564 # from Gnumed.pycommon import gmI18N 565 # from Gnumed.business import gmPerson 566 567 # gmI18N.activate_locale() 568 ## gmDateTime.init() 569
570 - def test_default_address():
571 bills = get_bills(pk_patient = 12) 572 first_bill = bills[0] 573 print(first_bill.default_address)
574
575 - def test_me():
576 print("--------------") 577 me = cBillable(aPK_obj=1) 578 fields = me.get_fields() 579 for field in fields: 580 print(field, ':', me[field]) 581 print("updatable:", me.get_updatable_fields())
582 #me['vat']=4; me.store_payload() 583 #-------------------------------------------------- 584 #test_me() 585 test_default_address() 586