Gnumed.business.gmBilling

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          #--------------------------------------------------------
    491 -        def set_missing_address_from_default(self):
    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