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

# Source Code for Module Gnumed.business.gmBilling

      1  # -*- coding: utf8 -*-
      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 = u'invoice'
     26  #============================================================
     27  # billables
     28  #------------------------------------------------------------
     29  _SQL_get_billable_fields = u"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 % u"pk_billable = %s"
     35          _cmds_store_payload = [
     36                  u"""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 = u'%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 += u' %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 += u' %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']], u'', ' - %s'),
     88                          self._payload[self._idx['catalog_long']]
     89                  )
     90                  txt += gmTools.coalesce(self._payload[self._idx['comment']], u'', u'\n %s')
     91
     92                  return txt
     93          #--------------------------------------------------------
     94 -        def _get_is_in_use(self):
     95                  cmd = u'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 = u' ORDER BY catalog_long, catalog_version, billable_code'
    106          else:
    107                  order_by = u' ORDER BY %s' % order_by
    108
    109          if active_only:
    110                  where = u'active IS true'
    111          else:
    112                  where = u'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 = u"""
    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 = u"""
    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 = u"""
    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 = u"SELECT * FROM bill.v_bill_items WHERE %s"
    178
    179 -class cBillItem(gmBusinessDBObject.cBusinessDBObject):
    180
    181          _cmd_fetch_payload = _SQL_fetch_bill_item_fields % u"pk_bill_item = %s"
    182          _cmds_store_payload = [
    183                  u"""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 = u'%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']], u'', ' - %s'),
    223                          self._payload[self._idx['pk_bill_item']]
    224                  )
    225                  txt += u' %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                          u'',
    232                          u'  (%s)\n',
    233                  )
    234                  txt += gmTools.coalesce (
    235                          self._payload[self._idx['item_detail']],
    236                          u'',
    237                          _(' Details: %s\n'),
    238                  )
    239
    240                  txt += u'\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 += u'\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 % u"pk_patient = %(pat)s AND pk_bill IS NULL"
    291          else:
    292                  cmd = _SQL_fetch_bill_item_fields % u"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 -def create_bill_item(pk_encounter=None, pk_billable=None, pk_staff=None):
    298
    299          billable = cBillable(aPK_obj = pk_billable)
    300          cmd = u"""
    301                  INSERT INTO bill.bill_item (
    302                          fk_provider,
    303                          fk_encounter,
    304                          net_amount_per_unit,
    305                          currency,
    306                          fk_billable
    307                  ) VALUES (
    308                          %(staff)s,
    309                          %(enc)s,
    310                          %(val)s,
    311                          %(curr)s,
    312                          %(billable)s
    313                  )
    314                  RETURNING pk"""
    315          args = {
    316                  'staff': pk_staff,
    317                  'enc': pk_encounter,
    318                  'val': billable['raw_amount'],
    319                  'curr': billable['currency'],
    320                  'billable': pk_billable
    321          }
    322          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
    323          return cBillItem(aPK_obj = rows[0][0])
    324  #------------------------------------------------------------
    325 -def delete_bill_item(pk_bill_item=None):
    326          cmd = u'DELETE FROM bill.bill_item WHERE pk = %(pk)s AND fk_bill IS NULL'
    327          args = {'pk': pk_bill_item}
    328          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    329
    330  #============================================================
    331  # bills
    332  #------------------------------------------------------------
    333  _SQL_get_bill_fields = u"""SELECT * FROM bill.v_bills WHERE %s"""
    334
    335 -class cBill(gmBusinessDBObject.cBusinessDBObject):
    336          """Represents a bill"""
    337
    338          _cmd_fetch_payload = _SQL_get_bill_fields % u"pk_bill = %s"
    339          _cmds_store_payload = [
    340                  u"""UPDATE bill.bill SET
    341                                  invoice_id = gm.nullify_empty_string(%(invoice_id)s),
    342                                  close_date = %(close_date)s,
    343                                  apply_vat = %(apply_vat)s,
    344                                  comment = gm.nullify_empty_string(%(comment)s),
    345                                  fk_receiver_identity = %(pk_receiver_identity)s,
    346                                  fk_receiver_address = %(pk_receiver_address)s,
    347                                  fk_doc = %(pk_doc)s
    348                          WHERE
    349                                  pk = %(pk_bill)s
    350                                          AND
    351                                  xmin = %(xmin_bill)s
    352                          RETURNING
    353                                  pk as pk_bill,
    354                                  xmin as xmin_bill
    355                  """
    356          ]
    357          _updatable_fields = [
    358                  u'invoice_id',
    359                  u'pk_receiver_identity',
    360                  u'close_date',
    361                  u'apply_vat',
    362                  u'comment',
    363                  u'pk_receiver_address',
    364                  u'pk_doc'
    365          ]
    366          #--------------------------------------------------------
    367 -        def format(self):
    368                  txt = u'%s                       [#%s]\n' % (
    369                          gmTools.bool2subst (
    370                                  (self._payload[self._idx['close_date']] is None),
    371                                  _('Open bill'),
    372                                  _('Closed bill')
    373                          ),
    374                          self._payload[self._idx['pk_bill']]
    375                  )
    376                  txt += _(' Invoice ID: %s\n') % self._payload[self._idx['invoice_id']]
    377
    378                  if self._payload[self._idx['close_date']] is not None:
    379                          txt += _(' Closed: %s\n') % gmDateTime.pydt_strftime (
    380                                  self._payload[self._idx['close_date']],
    381                                  '%Y %b %d',
    382                                  accuracy = gmDateTime.acc_days
    383                          )
    384
    385                  if self._payload[self._idx['comment']] is not None:
    386                          txt += _(' Comment: %s\n') % self._payload[self._idx['comment']]
    387
    388                  txt += _(' Bill value: %(curr)s%(val)s\n') % {
    389                          'curr': self._payload[self._idx['currency']],
    390                          'val': self._payload[self._idx['total_amount']]
    391                  }
    392
    393                  if self._payload[self._idx['apply_vat']]:
    394                          txt += _(' VAT: %(perc_vat)s%% %(equals)s %(curr)s%(vat)s\n') % {
    395                                  'perc_vat': self._payload[self._idx['percent_vat']],
    396                                  'equals': gmTools.u_corresponds_to,
    397                                  'curr': self._payload[self._idx['currency']],
    398                                  'vat': self._payload[self._idx['total_vat']]
    399                          }
    400                          txt += _(' Value + VAT: %(curr)s%(val)s\n') % {
    401                                  'curr': self._payload[self._idx['currency']],
    402                                  'val': self._payload[self._idx['total_amount_with_vat']]
    403                          }
    404                  else:
    405                          txt += _(' VAT: does not apply\n')
    406
    407                  if self._payload[self._idx['pk_bill_items']] is None:
    408                          txt += _(' Items billed: 0\n')
    409                  else:
    410                          txt += _(' Items billed: %s\n') % len(self._payload[self._idx['pk_bill_items']])
    411                  txt += _(' Invoice: %s\n') % (
    412                          gmTools.bool2subst (
    413                                  self._payload[self._idx['pk_doc']] is None,
    414                                  _('not available'),
    415                                  u'#%s' % self._payload[self._idx['pk_doc']]
    416                          )
    417                  )
    418                  txt += _(' Patient: #%s\n') % self._payload[self._idx['pk_patient']]
    419                  txt += gmTools.coalesce (
    420                          self._payload[self._idx['pk_receiver_identity']],
    421                          u'',
    422                          _(' Receiver: #%s\n')
    423                  )
    424                  if self._payload[self._idx['pk_receiver_address']] is not None:
    425                          txt += u'\n '.join(gmDemographicRecord.get_patient_address(pk_patient_address = self._payload[self._idx['pk_receiver_address']]).format())
    426
    427                  return txt
    428          #--------------------------------------------------------
    429 -        def add_items(self, items=None):
    430                  """Requires no pending changes within the bill itself."""
    431                  # should check for item consistency first
    432                  conn = gmPG2.get_connection(readonly = False)
    433                  for item in items:
    434                          item['pk_bill'] = self._payload[self._idx['pk_bill']]
    435                          item.save(conn = conn)
    436                  conn.commit()
    437                  self.refetch_payload()          # make sure aggregates are re-filled from view
    438          #--------------------------------------------------------
    439 -        def _get_bill_items(self):
    440                  return [ cBillItem(aPK_obj = pk) for pk in self._payload[self._idx['pk_bill_items']] ]
    441
    442          bill_items = property(_get_bill_items, lambda x:x)
    443          #--------------------------------------------------------
    444 -        def _get_invoice(self):
    445                  if self._payload[self._idx['pk_doc']] is None:
    446                          return None
    447                  return gmDocuments.cDocument(aPK_obj = self._payload[self._idx['pk_doc']])
    448
    449          invoice = property(_get_invoice, lambda x:x)
    450          #--------------------------------------------------------
    451 -        def _get_address(self):
    452                  if self._payload[self._idx['pk_receiver_address']] is None:
    453                          return None
    454                  return gmDemographicRecord.get_address_from_patient_address_pk (
    455                          pk_patient_address = self._payload[self._idx['pk_receiver_address']]
    456                  )
    457
    458          address = property(_get_address, lambda x:x)
    459          #--------------------------------------------------------
    460 -        def _get_default_address(self):
    461                  return gmDemographicRecord.get_patient_address_by_type (
    462                          pk_patient = self._payload[self._idx['pk_patient']],
    463                          adr_type = u'billing'
    464                  )
    465
    466          default_address = property(_get_default_address, lambda x:x)
    467          #--------------------------------------------------------
    468 -        def set_missing_address_from_default(self):
    469                  if self._payload[self._idx['pk_receiver_address']] is not None:
    470                          return True
    471                  adr = self.default_address
    472                  if adr is None:
    473                          return False
    474                  self['pk_receiver_address'] = adr['pk_lnk_person_org_address']
    475                  return self.save_payload()
    476  #------------------------------------------------------------
    477 -def get_bills(order_by=None, pk_patient=None):
    478
    479          args = {'pat': pk_patient}
    480          where_parts = [u'true']
    481
    482          if pk_patient is not None:
    483                  where_parts.append(u'pk_patient = %(pat)s')
    484
    485          if order_by is None:
    486                  order_by = u''
    487          else:
    488                  order_by = u' ORDER BY %s' % order_by
    489
    490          cmd = (_SQL_get_bill_fields % u' AND '.join(where_parts)) + order_by
    491          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    492          return [ cBill(row = {'data': r, 'idx': idx, 'pk_field': 'pk_bill'}) for r in rows ]
    493  #------------------------------------------------------------
    494 -def create_bill(conn=None, invoice_id=None):
    495
    496          args = {u'inv_id': invoice_id}
    497          cmd = u"""
    498                  INSERT INTO bill.bill (invoice_id)
    499                  VALUES (gm.nullify_empty_string(%(inv_id)s))
    500                  RETURNING pk
    501          """
    502          rows, idx = gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
    503
    504          return cBill(aPK_obj = rows[0]['pk'])
    505  #------------------------------------------------------------
    506 -def delete_bill(pk_bill=None):
    507          args = {'pk': pk_bill}
    508          cmd = u"DELETE FROM bill.bill WHERE pk = %(pk)s"
    509          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    510          return True
    511  #------------------------------------------------------------
    512 -def get_bill_receiver(pk_patient=None):
    513          pass
    514  #------------------------------------------------------------
    515 -def get_invoice_id(pk_patient=None):
    516          return u'GM%s / %s' % (
    517                  pk_patient,
    518                  gmDateTime.pydt_strftime (
    519                          gmDateTime.pydt_now_here(),
    520                          '%Y-%m-%d / %H%M%S'
    521                  )
    522          )
    523  #============================================================
    524  # main
    525  #------------------------------------------------------------
    526  if __name__ == "__main__":
    527
    528          if len(sys.argv) < 2:
    529                  sys.exit()
    530
    531          if sys.argv[1] != 'test':
    532                  sys.exit()
    533
    534  #       from Gnumed.pycommon import gmLog2
    535  #       from Gnumed.pycommon import gmI18N
    536  #       from Gnumed.business import gmPerson
    537
    538  #       gmI18N.activate_locale()
    539  ##      gmDateTime.init()
    540
    541 -        def test_default_address():
    542                  bills = get_bills(pk_patient = 12)
    543                  first_bill = bills[0]
    544                  print first_bill.default_address
    545
    546 -        def test_me():
    547                  print "--------------"
    548                  me = cBillable(aPK_obj=1)
    549                  fields = me.get_fields()
    550                  for field in fields:
    551                          print field, ':', me[field]
    552                  print "updatable:", me.get_updatable_fields()
    553                  #me['vat']=4; me.store_payload()
    554          #--------------------------------------------------
    555          #test_me()
    556          test_default_address()
    557