PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/payment.py

https://bitbucket.org/trytonspain/trytond-account_payment_sepa
Python | 394 lines | 387 code | 5 blank | 2 comment | 2 complexity | 85e64fa7312f31bcb854c8fd368fd19b MD5 | raw file
Possible License(s): GPL-3.0
  1. #This file is part of Tryton. The COPYRIGHT file at the top level of
  2. #this repository contains the full copyright notices and license terms.
  3. import datetime
  4. import os
  5. import genshi
  6. import genshi.template
  7. from sql import Literal
  8. from trytond.pool import PoolMeta, Pool
  9. from trytond.model import ModelSQL, ModelView, Workflow, fields
  10. from trytond.pyson import Eval, If
  11. from trytond.transaction import Transaction
  12. from trytond.tools import reduce_ids
  13. __metaclass__ = PoolMeta
  14. __all__ = ['Journal', 'Group', 'Payment', 'Mandate']
  15. class Journal:
  16. __name__ = 'account.payment.journal'
  17. company_party = fields.Function(fields.Many2One('party.party',
  18. 'Company Party', on_change_with=['company']),
  19. 'on_change_with_company_party')
  20. sepa_bank_account_number = fields.Many2One('bank.account.number',
  21. 'SEPA Bank Account Number', states={
  22. 'required': Eval('process_method').in_(['sepa_core', 'sepa_b2b',
  23. 'sepa_trf', 'sepa_chk']),
  24. 'invisible': ~Eval('process_method').in_(['sepa_core', 'sepa_b2b',
  25. 'sepa_trf', 'sepa_chk']),
  26. },
  27. domain=[
  28. ('type', '=', 'iban'),
  29. ('account.owners', '=', Eval('company_party')),
  30. ],
  31. depends=['process_method', 'company_party'])
  32. sepa_payable_flavor = fields.Selection([
  33. (None, ''),
  34. ('pain.001.001.03', 'pain.001.001.03'),
  35. ('pain.001.001.05', 'pain.001.001.05'),
  36. ], 'SEPA Payable Flavor', states={
  37. 'required': Eval('process_method').in_(['sepa_trf', 'sepa_chk']),
  38. 'invisible': ~Eval('process_method').in_(['sepa_trf', 'sepa_chk'])
  39. },
  40. depends=['process_method'])
  41. sepa_receivable_flavor = fields.Selection([
  42. (None, ''),
  43. ('pain.008.001.02', 'pain.008.001.02'),
  44. ('pain.008.001.04', 'pain.008.001.04'),
  45. ], 'SEPA Receivable Flavor', states={
  46. 'required': Eval('process_method').in_(['sepa_core', 'sepa_b2b']),
  47. 'invisible': ~Eval('process_method').in_(['sepa_core', 'sepa_b2b'])
  48. },
  49. depends=['process_method'])
  50. @classmethod
  51. def __setup__(cls):
  52. super(Journal, cls).__setup__()
  53. sepa_method = ('sepa_core', 'SEPA Core Direct Debit')
  54. if sepa_method not in cls.process_method.selection:
  55. cls.process_method.selection.append(sepa_method)
  56. sepa_method = ('sepa_b2b', 'SEPA B2B Direct Debit')
  57. if sepa_method not in cls.process_method.selection:
  58. cls.process_method.selection.append(sepa_method)
  59. sepa_method = ('sepa_trf', 'SEPA Credit Transfer')
  60. if sepa_method not in cls.process_method.selection:
  61. cls.process_method.selection.append(sepa_method)
  62. sepa_method = ('sepa_chk', 'SEPA Credit Check')
  63. if sepa_method not in cls.process_method.selection:
  64. cls.process_method.selection.append(sepa_method)
  65. @classmethod
  66. def default_company_party(cls):
  67. pool = Pool()
  68. Company = pool.get('company.company')
  69. company_id = cls.default_company()
  70. if company_id:
  71. return Company(company_id).party.id
  72. def on_change_with_company_party(self, name=None):
  73. if self.company:
  74. return self.company.party.id
  75. @property
  76. def sepa_method(self):
  77. if self.process_method == "sepa_core":
  78. return "CORE"
  79. elif self.process_method == "sepa_b2b":
  80. return "B2B"
  81. elif self.process_method == "sepa_trf":
  82. return "TRF"
  83. elif self.process_method == "sepa_chk":
  84. return "CHK"
  85. else:
  86. return ""
  87. def remove_comment(stream):
  88. for kind, data, pos in stream:
  89. if kind is genshi.core.COMMENT:
  90. continue
  91. yield kind, data, pos
  92. loader = genshi.template.TemplateLoader(
  93. os.path.join(os.path.dirname(__file__), 'template'),
  94. auto_reload=True)
  95. class Group:
  96. __name__ = 'account.payment.group'
  97. sepa_message = fields.Text('SEPA Message', readonly=True, states={
  98. 'invisible': ~Eval('sepa_message'),
  99. })
  100. sepa_file = fields.Function(fields.Binary('SEPA File',
  101. filename='sepa_filename', states={
  102. 'invisible': ~Eval('sepa_file'),
  103. }), 'get_sepa_file')
  104. sepa_filename = fields.Function(fields.Char('SEPA Filename'),
  105. 'get_sepa_filename')
  106. @classmethod
  107. def __setup__(cls):
  108. super(Group, cls).__setup__()
  109. cls._error_messages.update({
  110. 'no_mandate': 'No valid mandate for payment "%s"',
  111. })
  112. def get_sepa_file(self, name):
  113. if self.sepa_message:
  114. return buffer(self.sepa_message.encode('utf-8'))
  115. else:
  116. return ""
  117. def get_sepa_filename(self, name):
  118. return self.rec_name + '.xml'
  119. def get_sepa_template(self):
  120. if self.kind == 'payable':
  121. return loader.load('%s.xml' % self.journal.sepa_payable_flavor)
  122. elif self.kind == 'receivable':
  123. return loader.load('%s.xml' % self.journal.sepa_receivable_flavor)
  124. def process_sepa_core(self):
  125. self.process_sepa()
  126. def process_sepa_b2b(self):
  127. self.process_sepa()
  128. def process_sepa_trf(self):
  129. self.process_sepa()
  130. def process_sepa_chk(self):
  131. self.process_sepa()
  132. def process_sepa(self):
  133. pool = Pool()
  134. Payment = pool.get('account.payment')
  135. if self.kind == 'receivable':
  136. payments = [p for p in self.payments if not p.sepa_mandate]
  137. mandates = Payment.get_sepa_mandates(payments)
  138. for payment, mandate in zip(payments, mandates):
  139. if not mandate:
  140. self.raise_user_error('no_mandate', payment.rec_name)
  141. Payment.write([payment], {
  142. 'sepa_mandate': mandate,
  143. })
  144. tmpl = self.get_sepa_template()
  145. if not tmpl:
  146. raise NotImplementedError
  147. self.sepa_message = tmpl.generate(group=self,
  148. datetime=datetime).filter(remove_comment).render()
  149. @property
  150. def sepa_initiating_party(self):
  151. return self.company.party
  152. class Payment:
  153. __name__ = 'account.payment'
  154. sepa_mandate = fields.Many2One('account.payment.sepa.mandate', 'Mandate',
  155. ondelete='RESTRICT',
  156. domain=[
  157. ('party', '=', Eval('party', -1)),
  158. ],
  159. depends=['party'])
  160. @classmethod
  161. def get_sepa_mandates(cls, payments):
  162. mandates = []
  163. for payment in payments:
  164. for mandate in payment.party.sepa_mandates:
  165. if mandate.is_valid:
  166. break
  167. else:
  168. mandate = None
  169. mandates.append(mandate)
  170. return mandates
  171. @property
  172. def sepa_charge_bearer(self):
  173. return 'SLEV'
  174. @property
  175. def sepa_end_to_end_id(self):
  176. if self.line and self.line.origin:
  177. return self.line.origin.rec_name[:35]
  178. elif self.description:
  179. return self.description[:35]
  180. else:
  181. return str(self.id)
  182. @property
  183. def sepa_bank_account_number(self):
  184. for account in self.party.bank_accounts:
  185. for number in account.numbers:
  186. if number.type == 'iban':
  187. return number
  188. class Mandate(Workflow, ModelSQL, ModelView):
  189. 'SEPA Mandate'
  190. __name__ = 'account.payment.sepa.mandate'
  191. party = fields.Many2One('party.party', 'Party', required=True, select=True,
  192. states={
  193. 'readonly': Eval('state').in_(
  194. ['requested', 'validated', 'canceled']),
  195. },
  196. depends=['state'])
  197. account_number = fields.Many2One('bank.account.number', 'Account Number',
  198. states={
  199. 'readonly': Eval('state').in_(['validated', 'canceled']),
  200. 'required': Eval('state') == 'validated',
  201. },
  202. domain=[
  203. ('type', '=', 'iban'),
  204. ('account.owners', '=', Eval('party')),
  205. ],
  206. depends=['state', 'party'])
  207. identification = fields.Char('Identification', size=35,
  208. states={
  209. 'readonly': Eval('state').in_(['validated', 'canceled']),
  210. 'required': Eval('state') == 'validated',
  211. },
  212. depends=['state'])
  213. company = fields.Many2One('company.company', 'Company', required=True,
  214. select=True,
  215. domain=[
  216. ('id', If(Eval('context', {}).contains('company'), '=', '!='),
  217. Eval('context', {}).get('company', -1)),
  218. ],
  219. states={
  220. 'readonly': Eval('state') != 'draft',
  221. },
  222. depends=['state'])
  223. type = fields.Selection([
  224. ('recurrent', 'Recurrent'),
  225. ('one-off', 'One-off'),
  226. ], 'Type',
  227. states={
  228. 'readonly': Eval('state').in_(['validated', 'canceled']),
  229. },
  230. depends=['state'])
  231. signature_date = fields.Date('Signature Date',
  232. states={
  233. 'readonly': Eval('state').in_(['validated', 'canceled']),
  234. 'required': Eval('state') == 'validated',
  235. },
  236. depends=['state'])
  237. state = fields.Selection([
  238. ('draft', 'Draft'),
  239. ('requested', 'Requested'),
  240. ('validated', 'Validated'),
  241. ('canceled', 'Canceled'),
  242. ], 'State', readonly=True)
  243. payments = fields.One2Many('account.payment', 'sepa_mandate', 'Payments')
  244. has_payments = fields.Function(fields.Boolean('Has Payments'),
  245. 'has_payments')
  246. @classmethod
  247. def __setup__(cls):
  248. super(Mandate, cls).__setup__()
  249. cls._transitions |= set((
  250. ('draft', 'requested'),
  251. ('requested', 'validated'),
  252. ('validated', 'canceled'),
  253. ('requested', 'canceled'),
  254. ('requested', 'draft'),
  255. ))
  256. cls._buttons.update({
  257. 'cancel': {
  258. 'invisible': ~Eval('state').in_(
  259. ['requested', 'validated']),
  260. },
  261. 'draft': {
  262. 'invisible': Eval('state') != 'requested',
  263. },
  264. 'request': {
  265. 'invisible': Eval('state') != 'draft',
  266. },
  267. 'validate_mandate': {
  268. 'invisible': Eval('state') != 'requested',
  269. },
  270. })
  271. cls._error_messages.update({
  272. 'delete_draft_canceled': ('You can not delete mandate "%s" '
  273. 'because it is not in draft or canceled state.'),
  274. })
  275. @staticmethod
  276. def default_company():
  277. return Transaction().context.get('company')
  278. @staticmethod
  279. def default_type():
  280. return 'recurrent'
  281. @staticmethod
  282. def default_state():
  283. return 'draft'
  284. def get_rec_name(self, name):
  285. return self.identification or unicode(self.id)
  286. @property
  287. def is_valid(self):
  288. if self.state == 'validated':
  289. if self.type == 'one-off':
  290. if not self.has_payments:
  291. return True
  292. else:
  293. return True
  294. return False
  295. @property
  296. def sequence_type(self):
  297. if self.type == 'one-off':
  298. return 'OOFF'
  299. elif len(self.payments) == 1:
  300. return 'FRST'
  301. # TODO manage FNAL
  302. else:
  303. return 'RCUR'
  304. @classmethod
  305. def has_payments(self, mandates, name):
  306. pool = Pool()
  307. Payment = pool.get('account.payment')
  308. payment = Payment.__table__
  309. cursor = Transaction().cursor
  310. in_max = cursor.IN_MAX
  311. has_payments = dict.fromkeys([m.id for m in mandates], False)
  312. for i in range(0, len(mandates), in_max):
  313. sub_ids = [i.id for i in mandates[i:i + in_max]]
  314. red_sql = reduce_ids(payment.sepa_mandate, sub_ids)
  315. cursor.execute(*payment.select(payment.sepa_mandate, Literal(True),
  316. where=red_sql,
  317. group_by=payment.sepa_mandate))
  318. has_payments.update(cursor.fetchall())
  319. return {'has_payments': has_payments}
  320. @classmethod
  321. @ModelView.button
  322. @Workflow.transition('draft')
  323. def draft(cls, mandates):
  324. pass
  325. @classmethod
  326. @ModelView.button
  327. @Workflow.transition('requested')
  328. def request(cls, mandates):
  329. pass
  330. @classmethod
  331. @ModelView.button
  332. @Workflow.transition('validated')
  333. def validate_mandate(cls, mandates):
  334. pass
  335. @classmethod
  336. @ModelView.button
  337. @Workflow.transition('canceled')
  338. def cancel(cls, mandates):
  339. pass
  340. @classmethod
  341. def delete(cls, mandates):
  342. for mandate in mandates:
  343. if mandate.state not in ('draft', 'canceled'):
  344. cls.raise_user_error('delete_draft_canceled', mandate.rec_name)
  345. super(Mandate, cls).delete(mandates)