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  #------------------------------------------------------------
    633  # Remaining problems with invoice ID locking:
    634  #
    635  # If you run a 1.8.0rc1 client the lock can overflow PG's pg_try_advisory_lock().
    636  #
    637  # If you run both 1.8.0rc1 and 1.7 (<1.7.9) and happen to lock at the same
    638  # time the lock may succeed when it should not, because crc32/adler32 return
    639  # different representationel ranges in py2 and py3.
    640  #
    641  # If you run 1.7 (<1.7.9) on both "Python < 2.6" and "Python 2.6 or beyond"
    642  # and happen to lock at the same time the lock may succeed when it should
    643  # not, because crc32/adler32 return results with signedness depending on
    644  # platform.
    645  #------------------------------------------------------------
    646  #
    647  # remove in 1.9 / DB v23:
    648 -def __lock_invoice_id_1_7_legacy(invoice_id):
    649          """Get 1.7 legacy (<1.7.9) lock.
    650
    651          The fix is to *down*shift py3 checksums into the py2 result range.
    652          How to do that was suggested by MRAB on the Python mailing list.
    653
    654                  https://www.mail-archive.com/python-list@python.org/msg447989.html
    655
    656          Problems:
    657          - on py2 < 2.6 (client 1.7) signedness inconsistent across platforms
    658          - on py3 (client 1.8), range shifted by & 0xffffffff
    659
    660          Because both 1.7 (<1.7.9) and 1.8 can run against the same
    661          database v22 we need to retain this legacy lock until DB v23.
    662          """
    663          _log.debug('legacy locking invoice ID: %s', invoice_id)
    664          py3_crc32 = zlib.crc32(bytes(invoice_id, 'utf8'))
    665          py3_adler32 = zlib.adler32(bytes(invoice_id, 'utf8'))
    666          signed_crc32 = py3_crc32 - (py3_crc32 & 0x80000000) * 2
    667          signed_adler32 = py3_adler32 - (py3_adler32 & 0x80000000) * 2
    668          _log.debug('crc32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_crc32, signed_crc32)
    669          _log.debug('adler32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_adler32, signed_adler32)
    670          cmd = u"""SELECT pg_try_advisory_lock(%s, %s)""" % (signed_crc32, signed_adler32)
    671          try:
    672                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    673          except gmPG2.dbapi.ProgrammingError:
    674                  # should not happen
    675                  _log.exception('cannot lock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
    676                  return False
    677
    678          if rows[0][0]:
    679                  return True
    680
    681          _log.error('cannot lock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
    682          return False
    683
    684  #------------------------------------------------------------
    685 -def lock_invoice_id(invoice_id):
    686          """Lock an invoice ID.
    687
    688          The lock taken is an exclusive advisory lock in PostgreSQL.
    689
    690          Because the data is short _and_ crc32/adler32 are fairly
    691          weak we assume that collisions can be created "easily".
    692          Therefore we apply both algorithms concurrently.
    693          """
    694          _log.debug('locking invoice ID: %s', invoice_id)
    695          # remove in 1.9 / DB v23:
    696          if not __lock_invoice_id_1_7_legacy(invoice_id):
    697                  return False
    698
    699          # get py2/py3 compatible lock:
    700          # - upshift py2 result by & 0xffffffff for signedness consistency
    701          # - still use both crc32 and adler32 but chain the result of
    702          #   the former into the latter so we can take advantage of
    703          #   pg_try_advisory_lock(BIGINT)
    704          unsigned_crc32 = zlib.crc32(bytes(invoice_id, 'utf8')) & 0xffffffff
    705          _log.debug('unsigned crc32: %s', unsigned_crc32)
    706          data4adler32 = u'%s---[%s]' % (invoice_id, unsigned_crc32)
    707          _log.debug('data for adler32: %s', data4adler32)
    708          unsigned_adler32 = zlib.adler32(bytes(data4adler32, 'utf8'), unsigned_crc32) & 0xffffffff
    709          _log.debug('unsigned (crc32-chained) adler32: %s', unsigned_adler32)
    710          cmd = u"SELECT pg_try_advisory_lock(%s)" % (unsigned_adler32)
    711          try:
    712                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    713          except gmPG2.dbapi.ProgrammingError:
    714                  _log.exception('cannot lock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
    715                  return False
    716
    717          if rows[0][0]:
    718                  return True
    719
    720          _log.error('cannot lock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
    721          return False
    722
    723  #------------------------------------------------------------
    724 -def __unlock_invoice_id_1_7_legacy(invoice_id):
    725          _log.debug('legacy unlocking invoice ID: %s', invoice_id)
    726          py3_crc32 = zlib.crc32(bytes(invoice_id, 'utf8'))
    727          py3_adler32 = zlib.adler32(bytes(invoice_id, 'utf8'))
    728          signed_crc32 = py3_crc32 - (py3_crc32 & 0x80000000) * 2
    729          signed_adler32 = py3_adler32 - (py3_adler32 & 0x80000000) * 2
    730          _log.debug('crc32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_crc32, signed_crc32)
    731          _log.debug('adler32: %s (py3, unsigned) -> %s (py2.6+, signed)', py3_adler32, signed_adler32)
    732          cmd = u"""SELECT pg_advisory_unlock(%s, %s)""" % (signed_crc32, signed_adler32)
    733          try:
    734                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    735          except gmPG2.dbapi.ProgrammingError:
    736                  _log.exception('cannot unlock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
    737                  return False
    738
    739          if rows[0][0]:
    740                  return True
    741
    742          _log.error('cannot unlock invoice ID: [%s] (%s/%s)', invoice_id, signed_crc32, signed_adler32)
    743          return False
    744
    745  #------------------------------------------------------------
    746 -def unlock_invoice_id(invoice_id):
    747          _log.debug('unlocking invoice ID: %s', invoice_id)
    748          # remove in 1.9 / DB v23:
    749          if not __unlock_invoice_id_1_7_legacy(invoice_id):
    750                  return False
    751
    752          # unlock
    753          unsigned_crc32 = zlib.crc32(bytes(invoice_id, 'utf8')) & 0xffffffff
    754          _log.debug('unsigned crc32: %s', unsigned_crc32)
    755          data4adler32 = u'%s---[%s]' % (invoice_id, unsigned_crc32)
    756          _log.debug('data for adler32: %s', data4adler32)
    757          unsigned_adler32 = zlib.adler32(bytes(data4adler32, 'utf8'), unsigned_crc32) & 0xffffffff
    758          _log.debug('unsigned (crc32-chained) adler32: %s', unsigned_adler32)
    759          cmd = u"SELECT pg_advisory_unlock(%s)" % (unsigned_adler32)
    760          try:
    761                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    762          except gmPG2.dbapi.ProgrammingError:
    763                  _log.exception('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
    764                  return False
    765
    766          if rows[0][0]:
    767                  return True
    768
    769          _log.error('cannot unlock invoice ID: [%s] (%s)', invoice_id, unsigned_adler32)
    770          return False
    771
    772  #------------------------------------------------------------
    773 -def get_scan2pay_data(branch, bill, provider=None, comment=None):
    774          """Create scan2pay data for generating a QR code.
    775
    776          https://www.scan2pay.info
    777          --------------------------------
    778          BCD                                                                                                             # (3) fixed, barcode tag
    779          002                                                                                                             # (3) fixed, version
    780          1                                                                                                               # (1) charset, 1 = utf8
    781          SCT                                                                                                             # (3) fixed
    782          $<praxis_id::BIC//Bank//%(value)s::11>$                                 # (11) <BIC>
    783          $2<range_of::$<current_provider_name::%(lastnames)s::>$,$<praxis::%(praxis)s::>$::70>2$                 # (70) <Name of beneficiary> "Empfänger" - Praxis
    784          $<praxis_id::IBAN//Bank//%(value)s::34>$                                # (34) <IBAN>
    785          EUR$<bill::%(total_amount_with_vat)s::12>$                              # (12) <Amount in EURO> "EUR12.5"
    786                                                                                                                          # (4) <purpose of transfer> - leer
    787                                                                                                                          # (35) <remittance info - struct> - only this XOR the next field - GNUmed: leer
    788          $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"
    789          <beneficiary-to-payor info>                                                             # (70)  "pay soon :-)" - optional - GNUmed nur wenn bytes verfügbar
    790          --------------------------------
    791          total: 331 bytes (not chars ! - cave UTF8)
    792          EOL: LF or CRLF
    793          last *used* element not followed by anything, IOW can omit pending non-used elements
    794          """
    795          assert (branch is not None), '<branch> must not be <None>'
    796          assert (bill is not None), '<bill> must not be <None>'
    797
    798          data = {}
    799          IBANs = branch.get_external_ids(id_type = 'IBAN', issuer = 'Bank')
    800          if len(IBANs) == 0:
    801                  _log.debug('no IBAN found, cannot create scan2pay data')
    802                  return None
    803          data['IBAN'] = IBANs[0]['value'][:34]
    804          data['beneficiary'] = gmTools.coalesce (
    805                  value2test = provider,
    806                  return_instead = branch['praxis'][:70],
    807                  template4value = '%%(lastnames)s, %s' % branch['praxis']
    808          )[:70]
    809          BICs = branch.get_external_ids(id_type = 'BIC', issuer = 'Bank')
    810          if len(BICs) == 0:
    811                  data['BIC'] = ''
    812          else:
    813                  data['BIC'] = BICs[0]['value'][:11]
    814          data['amount'] = bill['total_amount_with_vat'][:9]
    815          data['ref'] = (_('Inv: %s, %s') % (
    816                  bill['invoice_id'],
    817                  gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%d.%B %Y')
    818          ))[:140]
    819          data['cmt'] = gmTools.coalesce(comment, '', '\n%s')[:70]
    820
    821          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
    822          data_str_bytes = bytes(data_str, 'utf8')[:331]
    823          return str(data_str_bytes, 'utf8')
    824
    825  #============================================================
    826  # main
    827  #------------------------------------------------------------
    828  if __name__ == "__main__":
    829
    830          if len(sys.argv) < 2:
    831                  sys.exit()
    832
    833          if sys.argv[1] != 'test':
    834                  sys.exit()
    835
    836  #       from Gnumed.pycommon import gmLog2
    837  #       from Gnumed.pycommon import gmI18N
    838  #       from Gnumed.business import gmPerson
    839          from Gnumed.business import gmPraxis
    840
    841  #       gmI18N.activate_locale()
    842          gmDateTime.init()
    843
    844 -        def test_default_address():
    845                  bills = get_bills(pk_patient = 12)
    846                  first_bill = bills[0]
    847                  print(first_bill.default_address)
    848
    849          #--------------------------------------------------
    850 -        def test_me():
    851                  print("--------------")
    852                  me = cBillable(aPK_obj=1)
    853                  fields = me.get_fields()
    854                  for field in fields:
    855                          print(field, ':', me[field])
    856                  print("updatable:", me.get_updatable_fields())
    857                  #me['vat']=4; me.store_payload()
    858
    859          #--------------------------------------------------
    860 -        def test_get_scan2pay_data():
    861                  prax = gmPraxis.get_praxis_branches()[0]
    862                  bills = get_bills(pk_patient = 12)
    863                  print(get_scan2pay_data (
    864                          prax,
    865                          bills[0],
    866                          provider=None,
    867                          comment = 'GNUmed test harness' + ('x' * 400)
    868                  ))
    869
    870          #--------------------------------------------------
    871 -        def test_generate_invoice_id():
    872                  from Gnumed.pycommon import gmI18N
    873                  gmI18N.activate_locale()
    874                  gmI18N.install_domain()
    875                  import gmPerson
    876                  for idx in range(1,15):
    877                          print ('')
    878                          print ('classic:', generate_invoice_id(pk_patient = idx))
    879                          pat = gmPerson.cPerson(idx)
    880                          template = u'%(firstname).4s%(lastname).4s%(date)s'
    881                          print ('new: template = "%s" => %s' % (
    882                                  template,
    883                                  generate_invoice_id (
    884                                          template = template,
    885                                          pk_patient = None,
    886                                          person = pat,
    887                                          date_format='%d%m%Y',
    888                                          time_format='%H%M%S'
    889                                  )
    890                          ))
    891                          template = u'%(firstname).4s%(lastname).4s%(date)s-#counter#'
    892                          new_id = generate_invoice_id (
    893                                  template = template,
    894                                  pk_patient = None,
    895                                  person = pat,
    896                                  date_format='%d%m%Y',
    897                                  time_format='%H%M%S'
    898                          )
    899                          print('locked: %s' % lock_invoice_id(new_id))
    900                          print('new: template = "%s" => %s' % (
    901                                  template,
    902                                  new_id
    903                          ))
    904                          print('unlocked: %s' % unlock_invoice_id(new_id))
    905
    906                  #generate_invoice_id(template=None, pk_patient=None, person=None, date_format='%Y-%m-%d', time_format='%H%M%S')
    907
    908          #--------------------------------------------------
    909
    910          #test_me()
    911          #test_default_address()
    912          #test_get_scan2pay_data()
    913          test_generate_invoice_id()
    914