You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
9.7 KiB

  1. # -*- coding: utf-8 -*-
  2. """Class to parse camt files."""
  3. ##############################################################################
  4. #
  5. # Copyright (C) 2013-2015 Therp BV <http://therp.nl>
  6. # All Rights Reserved
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as published
  10. # by the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ##############################################################################
  22. import re
  23. from datetime import datetime
  24. from lxml import etree
  25. from openerp.addons.bank_statement_parse.parserlib import (
  26. BankStatement,
  27. BankTransaction
  28. )
  29. class CamtParser(object):
  30. """Parser for camt bank statement import files."""
  31. def parse_amount(self, ns, node):
  32. """Parse element that contains Amount and CreditDebitIndicator."""
  33. if node is None:
  34. return 0.0
  35. sign = 1
  36. amount = 0.0
  37. sign_node = node.xpath('ns:CdtDbtInd', namespaces={'ns': ns})
  38. if sign_node and sign_node[0].text == 'DBIT':
  39. sign = -1
  40. amount_node = node.xpath('ns:Amt', namespaces={'ns': ns})
  41. if amount_node:
  42. amount = sign * float(amount_node[0].text)
  43. return amount
  44. def add_value_from_node(
  45. self, ns, node, xpath_str, obj, attr_name, join_str=None):
  46. """Add value to object from first or all nodes found with xpath.
  47. If xpath_str is a list (or iterable), it will be seen as a series
  48. of search path's in order of preference. The first item that results
  49. in a found node will be used to set a value."""
  50. if not isinstance(xpath_str, (list, tuple)):
  51. xpath_str = [xpath_str]
  52. for search_str in xpath_str:
  53. found_node = node.xpath(search_str, namespaces={'ns': ns})
  54. if found_node:
  55. if join_str is None:
  56. attr_value = found_node[0].text
  57. else:
  58. attr_value = join_str.join([x.text for x in found_node])
  59. setattr(obj, attr_name, attr_value)
  60. break
  61. def parse_transaction_details(self, ns, node, transaction):
  62. """Parse transaction details (message, party, account...)."""
  63. # message
  64. self.add_value_from_node(
  65. ns, node, [
  66. './ns:RmtInf/ns:Ustrd',
  67. './ns:AddtlTxInf',
  68. './ns:AddtlNtryInf',
  69. ], transaction, 'message')
  70. # eref
  71. self.add_value_from_node(
  72. ns, node, [
  73. './ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref',
  74. './ns:Refs/ns:EndToEndId',
  75. ],
  76. transaction, 'eref'
  77. )
  78. # remote party values
  79. party_type = 'Dbtr'
  80. party_type_node = node.xpath(
  81. '../../ns:CdtDbtInd', namespaces={'ns': ns})
  82. if party_type_node and party_type_node[0].text != 'CRDT':
  83. party_type = 'Cdtr'
  84. party_node = node.xpath(
  85. './ns:RltdPties/ns:%s' % party_type, namespaces={'ns': ns})
  86. if party_node:
  87. self.add_value_from_node(
  88. ns, party_node[0], './ns:Nm', transaction, 'remote_owner')
  89. self.add_value_from_node(
  90. ns, party_node[0], './ns:PstlAdr/ns:Ctry', transaction,
  91. 'remote_owner_country'
  92. )
  93. address_node = party_node[0].xpath(
  94. './ns:PstlAdr/ns:AdrLine', namespaces={'ns': ns})
  95. if address_node:
  96. transaction.remote_owner_address = [address_node[0].text]
  97. # Get remote_account from iban or from domestic account:
  98. account_node = node.xpath(
  99. './ns:RltdPties/ns:%sAcct/ns:Id' % party_type,
  100. namespaces={'ns': ns}
  101. )
  102. if account_node:
  103. iban_node = account_node[0].xpath(
  104. './ns:IBAN', namespaces={'ns': ns})
  105. if iban_node:
  106. transaction.remote_account = iban_node[0].text
  107. bic_node = node.xpath(
  108. './ns:RltdAgts/ns:%sAgt/ns:FinInstnId/ns:BIC' % party_type,
  109. namespaces={'ns': ns}
  110. )
  111. if bic_node:
  112. transaction.remote_bank_bic = bic_node[0].text
  113. else:
  114. self.add_value_from_node(
  115. ns, account_node[0], './ns:Othr/ns:Id', transaction,
  116. 'remote_account'
  117. )
  118. def parse_transaction(self, ns, node):
  119. """Parse transaction (entry) node."""
  120. transaction = BankTransaction()
  121. self.add_value_from_node(
  122. ns, node, './ns:BkTxCd/ns:Prtry/ns:Cd', transaction,
  123. 'transfer_type'
  124. )
  125. self.add_value_from_node(
  126. ns, node, './ns:BookgDt/ns:Dt', transaction, 'execution_date')
  127. self.add_value_from_node(
  128. ns, node, './ns:ValDt/ns:Dt', transaction, 'value_date')
  129. transaction.transferred_amount = self.parse_amount(ns, node)
  130. details_node = node.xpath(
  131. './ns:NtryDtls/ns:TxDtls', namespaces={'ns': ns})
  132. if details_node:
  133. self.parse_transaction_details(ns, details_node[0], transaction)
  134. transaction.data = etree.tostring(node)
  135. return transaction
  136. def get_balance_amounts(self, ns, node):
  137. """Return opening and closing balance.
  138. Depending on kind of balance and statement, the balance might be in a
  139. different kind of node:
  140. OPBD = OpeningBalance
  141. PRCD = PreviousClosingBalance
  142. ITBD = InterimBalance (first ITBD is start-, second is end-balance)
  143. CLBD = ClosingBalance
  144. """
  145. start_balance_node = None
  146. end_balance_node = None
  147. for node_name in ['OPBD', 'PRCD', 'CLBD', 'ITBD']:
  148. code_expr = (
  149. './ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]/../../..' %
  150. node_name
  151. )
  152. balance_node = node.xpath(code_expr, namespaces={'ns': ns})
  153. if balance_node:
  154. if node_name in ['OPBD', 'PRCD']:
  155. start_balance_node = balance_node[0]
  156. elif node_name == 'CLBD':
  157. end_balance_node = balance_node[0]
  158. else:
  159. if not start_balance_node:
  160. start_balance_node = balance_node[0]
  161. if not end_balance_node:
  162. end_balance_node = balance_node[-1]
  163. return (
  164. self.parse_amount(ns, start_balance_node),
  165. self.parse_amount(ns, end_balance_node)
  166. )
  167. def parse_statement(self, ns, node):
  168. """Parse a single Stmt node."""
  169. statement = BankStatement()
  170. self.add_value_from_node(
  171. ns, node, [
  172. './ns:Acct/ns:Id/ns:IBAN',
  173. './ns:Acct/ns:Id/ns:Othr/ns:Id',
  174. ], statement, 'local_account'
  175. )
  176. self.add_value_from_node(
  177. ns, node, './ns:Id', statement, 'statement_id')
  178. self.add_value_from_node(
  179. ns, node, './ns:Acct/ns:Ccy', statement, 'local_currency')
  180. (statement.start_balance, statement.end_balance) = (
  181. self.get_balance_amounts(ns, node))
  182. transaction_nodes = node.xpath('./ns:Ntry', namespaces={'ns': ns})
  183. for entry_node in transaction_nodes:
  184. transaction = self.parse_transaction(ns, entry_node)
  185. statement.transactions.append(transaction)
  186. if statement.transactions:
  187. statement.date = datetime.strptime(
  188. statement.transactions[0].execution_date, "%Y-%m-%d")
  189. return statement
  190. def check_version(self, ns, root):
  191. """Validate validity of camt file."""
  192. # Check wether it is camt at all:
  193. re_camt = re.compile(
  194. r'(^urn:iso:std:iso:20022:tech:xsd:camt.'
  195. r'|^ISO:camt.)'
  196. )
  197. if not re_camt.search(ns):
  198. raise ValueError('no camt: ' + ns)
  199. # Check wether version 052 or 053:
  200. re_camt_version = re.compile(
  201. r'(^urn:iso:std:iso:20022:tech:xsd:camt.053.'
  202. r'|^urn:iso:std:iso:20022:tech:xsd:camt.052.'
  203. r'|^ISO:camt.053.'
  204. r'|^ISO:camt.052.)'
  205. )
  206. if not re_camt_version.search(ns):
  207. raise ValueError('no camt 052 or 053: ' + ns)
  208. # Check GrpHdr element:
  209. root_0_0 = root[0][0].tag[len(ns) + 2:] # strip namespace
  210. if root_0_0 != 'GrpHdr':
  211. raise ValueError('expected GrpHdr, got: ' + root_0_0)
  212. def parse(self, data):
  213. """Parse a camt.052 or camt.053 file."""
  214. try:
  215. root = etree.fromstring(
  216. data, parser=etree.XMLParser(recover=True))
  217. except etree.XMLSyntaxError:
  218. # ABNAmro is known to mix up encodings
  219. root = etree.fromstring(
  220. data.decode('iso-8859-15').encode('utf-8'))
  221. if root is None:
  222. raise ValueError(
  223. 'Not a valid xml file, or not an xml file at all.')
  224. ns = root.tag[1:root.tag.index("}")]
  225. self.check_version(ns, root)
  226. statements = []
  227. for node in root[0][1:]:
  228. statement = self.parse_statement(ns, node)
  229. if len(statement.transactions):
  230. statements.append(statement)
  231. return statements
  232. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: