account_bank_statement_line.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842
  1. from odoo import api, Command, fields, models, _
  2. from odoo.exceptions import UserError, ValidationError
  3. from odoo.tools import html2plaintext
  4. from odoo.addons.base.models.res_bank import sanitize_account_number
  5. from xmlrpc.client import MAXINT
  6. from itertools import product
  7. class AccountBankStatementLine(models.Model):
  8. _name = "account.bank.statement.line"
  9. _inherits = {'account.move': 'move_id'}
  10. _description = "Bank Statement Line"
  11. _order = "internal_index desc"
  12. _check_company_auto = True
  13. # FIXME: Field having the same name in both tables are confusing (partner_id). We don't change it because:
  14. # - It's a mess to track/fix.
  15. # - Some fields here could be simplified when the onchanges will be gone in account.move.
  16. # Should be improved in the future.
  17. # - there should be a better way for syncing account_moves with bank transactions, payments, invoices, etc.
  18. # == Business fields ==
  19. def default_get(self, fields_list):
  20. defaults = super().default_get(fields_list)
  21. # copy the date and statement from the latest transaction of the same journal to help the user
  22. # to enter the next transaction, they do not have to enter the date and the statement every time until the
  23. # statement is completed. It is only possible if we know the journal that is used, so it can only be done
  24. # in a view in which the journal is already set and so is single journal view.
  25. if 'journal_id' in defaults and 'date' in fields_list:
  26. last_line = self.search([
  27. ('journal_id', '=', defaults.get('journal_id')),
  28. ('state', '=', 'posted'),
  29. ], limit=1)
  30. statement = last_line.statement_id
  31. if statement:
  32. defaults.setdefault('date', statement.date)
  33. elif last_line:
  34. defaults.setdefault('date', last_line.date)
  35. return defaults
  36. move_id = fields.Many2one(
  37. comodel_name='account.move',
  38. auto_join=True,
  39. string='Journal Entry', required=True, readonly=True, ondelete='cascade',
  40. check_company=True)
  41. statement_id = fields.Many2one(
  42. comodel_name='account.bank.statement',
  43. string='Statement',
  44. )
  45. # Payments generated during the reconciliation of this bank statement lines.
  46. payment_ids = fields.Many2many(
  47. comodel_name='account.payment',
  48. relation='account_payment_account_bank_statement_line_rel',
  49. string='Auto-generated Payments',
  50. )
  51. # This sequence is working reversed because the default order is reversed, more info in compute_internal_index
  52. sequence = fields.Integer(default=1)
  53. partner_id = fields.Many2one(
  54. comodel_name='res.partner',
  55. string='Partner', ondelete='restrict',
  56. domain="['|', ('parent_id','=', False), ('is_company','=',True)]",
  57. check_company=True)
  58. # Technical field used to store the bank account number before its creation, upon the line's processing
  59. account_number = fields.Char(string='Bank Account Number')
  60. # This field is used to record the third party name when importing bank statement in electronic format,
  61. # when the partner doesn't exist yet in the database (or cannot be found).
  62. partner_name = fields.Char()
  63. # Transaction type is used in electronic format, when the type of transaction is available in the imported file.
  64. transaction_type = fields.Char()
  65. payment_ref = fields.Char(string='Label')
  66. currency_id = fields.Many2one(
  67. comodel_name='res.currency',
  68. string='Journal Currency',
  69. compute='_compute_currency_id', store=True,
  70. )
  71. amount = fields.Monetary()
  72. # Note the values of this field does not necessarily correspond to the cumulated balance in the account move line.
  73. # here these values correspond to occurrence order (the reality) and they should match the bank report but in
  74. # the move lines, it corresponds to the recognition order. Also, the statements act as checkpoints on this field
  75. running_balance = fields.Monetary(
  76. compute='_compute_running_balance'
  77. )
  78. foreign_currency_id = fields.Many2one(
  79. comodel_name='res.currency',
  80. string="Foreign Currency",
  81. help="The optional other currency if it is a multi-currency entry.",
  82. )
  83. amount_currency = fields.Monetary(
  84. compute='_compute_amount_currency', store=True, readonly=False,
  85. string="Amount in Currency",
  86. currency_field='foreign_currency_id',
  87. help="The amount expressed in an optional other currency if it is a multi-currency entry.",
  88. )
  89. # == Technical fields ==
  90. # The amount left to be reconciled on this statement line (signed according to its move lines' balance),
  91. # expressed in its currency. This is a technical field use to speed up the application of reconciliation models.
  92. amount_residual = fields.Float(
  93. string="Residual Amount",
  94. compute="_compute_is_reconciled",
  95. store=True,
  96. )
  97. country_code = fields.Char(
  98. related='company_id.account_fiscal_country_id.code'
  99. )
  100. # Technical field used to store the internal reference of the statement line for fast indexing and easier comparing
  101. # of statement lines. It holds the combination of the date, sequence and id of each line. Without this field,
  102. # the search/sorting lines would be very slow. The date field is related and stored in the account.move model,
  103. # so it is not possible to have an index on it (unless we use a sql view which is too complicated).
  104. # Using this prevents us having a compound index, and extensive `where` clauses.
  105. # Without this finding lines before current line (which we need e.g. for calculating the running balance)
  106. # would need a query like this:
  107. # date < current date OR (date = current date AND sequence > current date) or (
  108. # date = current date AND sequence = current sequence AND id < current id)
  109. # which needs to be repeated all over the code.
  110. # This would be simply "internal index < current internal index" using this field.
  111. internal_index = fields.Char(
  112. string='Internal Reference',
  113. compute='_compute_internal_index', store=True,
  114. index=True,
  115. )
  116. # Technical field indicating if the statement line is already reconciled.
  117. is_reconciled = fields.Boolean(
  118. string='Is Reconciled',
  119. compute='_compute_is_reconciled', store=True,
  120. )
  121. statement_complete = fields.Boolean(
  122. related='statement_id.is_complete',
  123. )
  124. statement_valid = fields.Boolean(
  125. related='statement_id.is_valid',
  126. )
  127. # -------------------------------------------------------------------------
  128. # COMPUTE METHODS
  129. # -------------------------------------------------------------------------
  130. @api.depends('foreign_currency_id', 'date', 'amount', 'company_id')
  131. def _compute_amount_currency(self):
  132. for st_line in self:
  133. if not st_line.foreign_currency_id:
  134. st_line.amount_currency = False
  135. elif st_line.date and not st_line.amount_currency:
  136. # only convert if it hasn't been set already
  137. st_line.amount_currency = st_line.currency_id._convert(
  138. from_amount=st_line.amount,
  139. to_currency=st_line.foreign_currency_id,
  140. company=st_line.company_id,
  141. date=st_line.date,
  142. )
  143. @api.depends('journal_id.currency_id')
  144. def _compute_currency_id(self):
  145. for st_line in self:
  146. st_line.currency_id = st_line.journal_id.currency_id or st_line.company_id.currency_id
  147. def _compute_running_balance(self):
  148. # It looks back to find the latest statement and uses its balance_start as an anchor point for calculation, so
  149. # that the running balance is always relative to the latest statement. In this way we do not need to calculate
  150. # the running balance for all statement lines every time.
  151. # If there are statements inside the computed range, their balance_start has priority over calculated balance.
  152. # we have to compute running balance for draft lines because they are visible and also
  153. # the user can split on that lines, but their balance should be the same as previous posted line
  154. # we do the same for the canceled lines, in order to keep using them as anchor points
  155. self.statement_id.flush_model(['balance_start', 'first_line_index'])
  156. self.flush_model(['internal_index', 'date', 'journal_id', 'statement_id', 'amount', 'state'])
  157. record_by_id = {x.id: x for x in self}
  158. for journal in self.journal_id:
  159. journal_lines_indexes = self.filtered(lambda line: line.journal_id == journal)\
  160. .sorted('internal_index')\
  161. .mapped('internal_index')
  162. min_index, max_index = journal_lines_indexes[0], journal_lines_indexes[-1]
  163. # Find the oldest index for each journal.
  164. self._cr.execute(
  165. """
  166. SELECT first_line_index, COALESCE(balance_start, 0.0)
  167. FROM account_bank_statement
  168. WHERE
  169. first_line_index < %s
  170. AND journal_id = %s
  171. ORDER BY first_line_index DESC
  172. LIMIT 1
  173. """,
  174. [min_index, journal.id],
  175. )
  176. current_running_balance = 0.0
  177. extra_clause = ''
  178. extra_params = []
  179. row = self._cr.fetchone()
  180. if row:
  181. starting_index, current_running_balance = row
  182. extra_clause = "AND st_line.internal_index >= %s"
  183. extra_params.append(starting_index)
  184. self._cr.execute(
  185. f"""
  186. SELECT
  187. st_line.id,
  188. st_line.amount,
  189. st.first_line_index = st_line.internal_index AS is_anchor,
  190. COALESCE(st.balance_start, 0.0),
  191. move.state
  192. FROM account_bank_statement_line st_line
  193. JOIN account_move move ON move.id = st_line.move_id
  194. LEFT JOIN account_bank_statement st ON st.id = st_line.statement_id
  195. WHERE
  196. st_line.internal_index <= %s
  197. AND move.journal_id = %s
  198. {extra_clause}
  199. ORDER BY st_line.internal_index
  200. """,
  201. [max_index, journal.id] + extra_params,
  202. )
  203. for st_line_id, amount, is_anchor, balance_start, state in self._cr.fetchall():
  204. if is_anchor:
  205. current_running_balance = balance_start
  206. if state == 'posted':
  207. current_running_balance += amount
  208. if record_by_id.get(st_line_id):
  209. record_by_id[st_line_id].running_balance = current_running_balance
  210. @api.depends('date', 'sequence')
  211. def _compute_internal_index(self):
  212. """
  213. Internal index is a field that holds the combination of the date, compliment of sequence and id of each line.
  214. Using this prevents us having a compound index, and extensive where clauses.
  215. Without this finding lines before current line (which we need for calculating the running balance)
  216. would need a query like this:
  217. date < current date OR (date = current date AND sequence > current date) or (
  218. date = current date AND sequence = current sequence AND id < current id)
  219. which needs to be repeated all over the code.
  220. This would be simply "internal index < current internal index" using this field.
  221. Also, we would need a compound index of date + sequence + id
  222. on the table which is not possible because date is not in this table (it is in the account move table)
  223. unless we use a sql view which is more complicated.
  224. """
  225. # ensure we are using correct value for reversing sequence in the index (2147483647)
  226. # NOTE: assert self._fields['sequence'].column_type[1] == 'int4'
  227. # if for any reason it changes (how unlikely), we need to update this code
  228. for st_line in self.filtered(lambda line: line._origin.id):
  229. st_line.internal_index = f'{st_line.date.strftime("%Y%m%d")}' \
  230. f'{MAXINT - st_line.sequence:0>10}' \
  231. f'{st_line._origin.id:0>10}'
  232. @api.depends('journal_id', 'currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
  233. 'move_id.to_check',
  234. 'move_id.line_ids.account_id', 'move_id.line_ids.amount_currency',
  235. 'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.currency_id',
  236. 'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids')
  237. def _compute_is_reconciled(self):
  238. """ Compute the field indicating if the statement lines are already reconciled with something.
  239. This field is used for display purpose (e.g. display the 'cancel' button on the statement lines).
  240. Also computes the residual amount of the statement line.
  241. """
  242. for st_line in self:
  243. _liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines()
  244. # Compute residual amount
  245. if st_line.to_check:
  246. st_line.amount_residual = -st_line.amount_currency if st_line.foreign_currency_id else -st_line.amount
  247. elif suspense_lines.account_id.reconcile:
  248. st_line.amount_residual = sum(suspense_lines.mapped('amount_residual_currency'))
  249. else:
  250. st_line.amount_residual = sum(suspense_lines.mapped('amount_currency'))
  251. # Compute is_reconciled
  252. if not st_line.id:
  253. # New record: The journal items are not yet there.
  254. st_line.is_reconciled = False
  255. elif suspense_lines:
  256. # In case of the statement line comes from an older version, it could have a residual amount of zero.
  257. st_line.is_reconciled = suspense_lines.currency_id.is_zero(st_line.amount_residual)
  258. elif st_line.currency_id.is_zero(st_line.amount):
  259. st_line.is_reconciled = True
  260. else:
  261. # The journal entry seems reconciled.
  262. st_line.is_reconciled = True
  263. # -------------------------------------------------------------------------
  264. # CONSTRAINT METHODS
  265. # -------------------------------------------------------------------------
  266. @api.constrains('amount', 'amount_currency', 'currency_id', 'foreign_currency_id', 'journal_id')
  267. def _check_amounts_currencies(self):
  268. """ Ensure the consistency the specified amounts and the currencies. """
  269. for st_line in self:
  270. if st_line.foreign_currency_id == st_line.currency_id:
  271. raise ValidationError(_("The foreign currency must be different than the journal one: %s",
  272. st_line.currency_id.name))
  273. if not st_line.foreign_currency_id and st_line.amount_currency:
  274. raise ValidationError(_("You can't provide an amount in foreign currency without "
  275. "specifying a foreign currency."))
  276. if not st_line.amount_currency and st_line.foreign_currency_id:
  277. raise ValidationError(_("You can't provide a foreign currency without specifying an amount in "
  278. "'Amount in Currency' field."))
  279. # -------------------------------------------------------------------------
  280. # LOW-LEVEL METHODS
  281. # -------------------------------------------------------------------------
  282. def new(self, values=None, origin=None, ref=None):
  283. st_line = super().new(values, origin, ref)
  284. if not st_line.journal_id: # might not be computed because declared by inheritance
  285. st_line.move_id._compute_journal_id()
  286. return st_line
  287. @api.model_create_multi
  288. def create(self, vals_list):
  289. # OVERRIDE
  290. counterpart_account_ids = []
  291. for vals in vals_list:
  292. if 'statement_id' in vals and 'journal_id' not in vals:
  293. statement = self.env['account.bank.statement'].browse(vals['statement_id'])
  294. # Ensure the journal is the same as the statement one.
  295. # journal_id is a required field in the view, so it should be always available if the user
  296. # is creating the record, however, if a sync/import modules tries to add a line to an existing
  297. # statement they can omit the journal field because it can be obtained from the statement
  298. if statement.journal_id:
  299. vals['journal_id'] = statement.journal_id.id
  300. # Avoid having the same foreign_currency_id as currency_id.
  301. if vals.get('journal_id') and vals.get('foreign_currency_id'):
  302. journal = self.env['account.journal'].browse(vals['journal_id'])
  303. journal_currency = journal.currency_id or journal.company_id.currency_id
  304. if vals['foreign_currency_id'] == journal_currency.id:
  305. vals['foreign_currency_id'] = None
  306. vals['amount_currency'] = 0.0
  307. # Force the move_type to avoid inconsistency with residual 'default_move_type' inside the context.
  308. vals['move_type'] = 'entry'
  309. # Hack to force different account instead of the suspense account.
  310. counterpart_account_ids.append(vals.pop('counterpart_account_id', None))
  311. #Set the amount to 0 if it's not specified.
  312. if 'amount' not in vals:
  313. vals['amount'] = 0
  314. st_lines = super().create(vals_list)
  315. for i, st_line in enumerate(st_lines):
  316. counterpart_account_id = counterpart_account_ids[i]
  317. to_write = {'statement_line_id': st_line.id, 'narration': st_line.narration}
  318. if 'line_ids' not in vals_list[i]:
  319. to_write['line_ids'] = [(0, 0, line_vals) for line_vals in st_line._prepare_move_line_default_vals(
  320. counterpart_account_id=counterpart_account_id)]
  321. st_line.move_id.write(to_write)
  322. # Otherwise field narration will be recomputed silently (at next flush) when writing on partner_id
  323. self.env.remove_to_compute(st_line.move_id._fields['narration'], st_line.move_id)
  324. # No need for the user to manage their status (from 'Draft' to 'Posted')
  325. st_lines.move_id.action_post()
  326. return st_lines
  327. def write(self, vals):
  328. # OVERRIDE
  329. res = super().write(vals)
  330. self._synchronize_to_moves(set(vals.keys()))
  331. return res
  332. def unlink(self):
  333. # OVERRIDE to unlink the inherited account.move (move_id field) as well.
  334. moves = self.with_context(force_delete=True).mapped('move_id')
  335. res = super().unlink()
  336. moves.unlink()
  337. return res
  338. @api.model
  339. def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
  340. # Add latest running_balance in the read_group
  341. result = super(AccountBankStatementLine, self).read_group(
  342. domain, fields, groupby, offset=offset,
  343. limit=limit, orderby=orderby, lazy=lazy)
  344. show_running_balance = False
  345. # We loop over the content of groupby because the groupby date is in the form of "date:granularity"
  346. for el in groupby:
  347. if (el == 'statement_id' or el == 'journal_id' or el.startswith('date')) and 'running_balance' in fields:
  348. show_running_balance = True
  349. break
  350. if show_running_balance:
  351. for group_line in result:
  352. group_line['running_balance'] = self.search(group_line.get('__domain'), limit=1).running_balance or 0.0
  353. return result
  354. # -------------------------------------------------------------------------
  355. # ACTION METHODS
  356. # -------------------------------------------------------------------------
  357. def action_undo_reconciliation(self):
  358. """ Undo the reconciliation made on the statement line and reset their journal items
  359. to their original states.
  360. """
  361. self.line_ids.remove_move_reconcile()
  362. self.payment_ids.unlink()
  363. for st_line in self:
  364. st_line.with_context(force_delete=True).write({
  365. 'to_check': False,
  366. 'line_ids': [Command.clear()] + [
  367. Command.create(line_vals) for line_vals in st_line._prepare_move_line_default_vals()],
  368. })
  369. # -------------------------------------------------------------------------
  370. # HELPERS
  371. # -------------------------------------------------------------------------
  372. def _find_or_create_bank_account(self):
  373. self.ensure_one()
  374. # There is a sql constraint on res.partner.bank ensuring an unique pair <partner, account number>.
  375. # Since it's not dependent of the company, we need to search on others company too to avoid the creation
  376. # of an extra res.partner.bank raising an error coming from this constraint.
  377. # However, at the end, we need to filter out the results to not trigger the check_company when trying to
  378. # assign a res.partner.bank owned by another company.
  379. bank_account = self.env['res.partner.bank'].sudo().with_context(active_test=False).search([
  380. ('acc_number', '=', self.account_number),
  381. ('partner_id', '=', self.partner_id.id),
  382. ])
  383. if not bank_account:
  384. bank_account = self.env['res.partner.bank'].create({
  385. 'acc_number': self.account_number,
  386. 'partner_id': self.partner_id.id,
  387. 'journal_id': None,
  388. })
  389. return bank_account.filtered(lambda x: x.company_id in (False, self.company_id))
  390. def _get_amounts_with_currencies(self):
  391. """
  392. Returns the line amount in company, journal and foreign currencies
  393. """
  394. self.ensure_one()
  395. company_currency = self.journal_id.company_id.currency_id
  396. journal_currency = self.journal_id.currency_id or company_currency
  397. foreign_currency = self.foreign_currency_id or journal_currency or company_currency
  398. journal_amount = self.amount
  399. if foreign_currency == journal_currency:
  400. transaction_amount = journal_amount
  401. else:
  402. transaction_amount = self.amount_currency
  403. if journal_currency == company_currency:
  404. company_amount = journal_amount
  405. elif foreign_currency == company_currency:
  406. company_amount = transaction_amount
  407. else:
  408. company_amount = journal_currency._convert(journal_amount, company_currency,
  409. self.journal_id.company_id, self.date)
  410. return company_amount, company_currency, journal_amount, journal_currency, transaction_amount, foreign_currency
  411. def _get_default_amls_matching_domain(self):
  412. return [
  413. # Base domain.
  414. ('display_type', 'not in', ('line_section', 'line_note')),
  415. ('parent_state', '=', 'posted'),
  416. ('company_id', '=', self.company_id.id),
  417. # Reconciliation domain.
  418. ('reconciled', '=', False),
  419. ('account_id.reconcile', '=', True),
  420. # Special domain for payments.
  421. '|',
  422. ('account_id.account_type', 'not in', ('asset_receivable', 'liability_payable')),
  423. ('payment_id', '=', False),
  424. # Special domain for statement lines.
  425. ('statement_line_id', '!=', self.id),
  426. ]
  427. @api.model
  428. def _get_default_journal(self):
  429. journal_type = self.env.context.get('journal_type', 'bank')
  430. return self.env['account.journal'].search([
  431. ('type', '=', journal_type),
  432. ('company_id', '=', self.env.company.id)
  433. ], limit=1)
  434. def _get_st_line_strings_for_matching(self, allowed_fields=None):
  435. """ Collect the strings that could be used on the statement line to perform some matching.
  436. :param allowed_fields: A explicit list of fields to consider.
  437. :return: A list of strings.
  438. """
  439. self.ensure_one()
  440. st_line_text_values = []
  441. if not allowed_fields or 'payment_ref' in allowed_fields:
  442. if self.payment_ref:
  443. st_line_text_values.append(self.payment_ref)
  444. if not allowed_fields or 'narration' in allowed_fields:
  445. value = html2plaintext(self.narration or "")
  446. if value:
  447. st_line_text_values.append(value)
  448. if not allowed_fields or 'ref' in allowed_fields:
  449. if self.ref:
  450. st_line_text_values.append(self.ref)
  451. return st_line_text_values
  452. def _get_accounting_amounts_and_currencies(self):
  453. """ Retrieve the transaction amount, journal amount and the company amount with their corresponding currencies
  454. from the journal entry linked to the statement line.
  455. All returned amounts will be positive for an inbound transaction, negative for an outbound one.
  456. :return: (
  457. transaction_amount, transaction_currency,
  458. journal_amount, journal_currency,
  459. company_amount, company_currency,
  460. )
  461. """
  462. self.ensure_one()
  463. liquidity_line, suspense_line, other_lines = self._seek_for_lines()
  464. if suspense_line and not other_lines:
  465. transaction_amount = -suspense_line.amount_currency
  466. transaction_currency = suspense_line.currency_id
  467. else:
  468. # In case of to_check or partial reconciliation, we can't trust the suspense line.
  469. transaction_amount = self.amount_currency if self.foreign_currency_id else self.amount
  470. transaction_currency = self.foreign_currency_id or liquidity_line.currency_id
  471. return (
  472. transaction_amount,
  473. transaction_currency,
  474. sum(liquidity_line.mapped('amount_currency')),
  475. liquidity_line.currency_id,
  476. sum(liquidity_line.mapped('balance')),
  477. liquidity_line.company_currency_id,
  478. )
  479. def _prepare_counterpart_amounts_using_st_line_rate(self, currency, balance, amount_currency):
  480. """ Convert the amounts passed as parameters to the statement line currency using the rates provided by the
  481. bank. The computed amounts are the one that could be set on the statement line as a counterpart journal item
  482. to fully paid the provided amounts as parameters.
  483. :param currency: The currency in which is expressed 'amount_currency'.
  484. :param balance: The amount expressed in company currency. Only needed when the currency passed as
  485. parameter is neither the statement line's foreign currency, neither the journal's
  486. currency.
  487. :param amount_currency: The amount expressed in the 'currency' passed as parameter.
  488. :return: A python dictionary containing:
  489. * balance: The amount to consider expressed in company's currency.
  490. * amount_currency: The amount to consider expressed in statement line's foreign currency.
  491. """
  492. self.ensure_one()
  493. transaction_amount, transaction_currency, journal_amount, journal_currency, company_amount, company_currency \
  494. = self._get_accounting_amounts_and_currencies()
  495. rate_journal2foreign_curr = journal_amount and abs(transaction_amount) / abs(journal_amount)
  496. rate_comp2journal_curr = company_amount and abs(journal_amount) / abs(company_amount)
  497. if currency == transaction_currency:
  498. trans_amount_currency = amount_currency
  499. if rate_journal2foreign_curr:
  500. journ_amount_currency = journal_currency.round(trans_amount_currency / rate_journal2foreign_curr)
  501. else:
  502. journ_amount_currency = 0.0
  503. if rate_comp2journal_curr:
  504. new_balance = company_currency.round(journ_amount_currency / rate_comp2journal_curr)
  505. else:
  506. new_balance = 0.0
  507. elif currency == journal_currency:
  508. trans_amount_currency = transaction_currency.round(amount_currency * rate_journal2foreign_curr)
  509. if rate_comp2journal_curr:
  510. new_balance = company_currency.round(amount_currency / rate_comp2journal_curr)
  511. else:
  512. new_balance = 0.0
  513. else:
  514. journ_amount_currency = journal_currency.round(balance * rate_comp2journal_curr)
  515. trans_amount_currency = transaction_currency.round(journ_amount_currency * rate_journal2foreign_curr)
  516. new_balance = balance
  517. return {
  518. 'amount_currency': trans_amount_currency,
  519. 'balance': new_balance,
  520. }
  521. def _prepare_move_line_default_vals(self, counterpart_account_id=None):
  522. """ Prepare the dictionary to create the default account.move.lines for the current account.bank.statement.line
  523. record.
  524. :return: A list of python dictionary to be passed to the account.move.line's 'create' method.
  525. """
  526. self.ensure_one()
  527. if not counterpart_account_id:
  528. counterpart_account_id = self.journal_id.suspense_account_id.id
  529. if not counterpart_account_id:
  530. raise UserError(_(
  531. "You can't create a new statement line without a suspense account set on the %s journal.",
  532. self.journal_id.display_name,
  533. ))
  534. company_amount, _company_currency, journal_amount, journal_currency, transaction_amount, foreign_currency \
  535. = self._get_amounts_with_currencies()
  536. liquidity_line_vals = {
  537. 'name': self.payment_ref,
  538. 'move_id': self.move_id.id,
  539. 'partner_id': self.partner_id.id,
  540. 'account_id': self.journal_id.default_account_id.id,
  541. 'currency_id': journal_currency.id,
  542. 'amount_currency': journal_amount,
  543. 'debit': company_amount > 0 and company_amount or 0.0,
  544. 'credit': company_amount < 0 and -company_amount or 0.0,
  545. }
  546. # Create the counterpart line values.
  547. counterpart_line_vals = {
  548. 'name': self.payment_ref,
  549. 'account_id': counterpart_account_id,
  550. 'move_id': self.move_id.id,
  551. 'partner_id': self.partner_id.id,
  552. 'currency_id': foreign_currency.id,
  553. 'amount_currency': -transaction_amount,
  554. 'debit': -company_amount if company_amount < 0.0 else 0.0,
  555. 'credit': company_amount if company_amount > 0.0 else 0.0,
  556. }
  557. return [liquidity_line_vals, counterpart_line_vals]
  558. def _retrieve_partner(self):
  559. self.ensure_one()
  560. # Retrieve the partner from the statement line.
  561. if self.partner_id:
  562. return self.partner_id
  563. # Retrieve the partner from the bank account.
  564. if self.account_number:
  565. account_number_nums = sanitize_account_number(self.account_number)
  566. if account_number_nums:
  567. domain = [('sanitized_acc_number', 'ilike', account_number_nums)]
  568. for extra_domain in ([('company_id', '=', self.company_id.id)], []):
  569. bank_accounts = self.env['res.partner.bank'].search(extra_domain + domain)
  570. if len(bank_accounts.partner_id) == 1:
  571. return bank_accounts.partner_id
  572. # Retrieve the partner from the partner name.
  573. if self.partner_name:
  574. domains = product(
  575. [
  576. ('name', '=ilike', self.partner_name),
  577. ('name', 'ilike', self.partner_name),
  578. ],
  579. [
  580. ('company_id', '=', self.company_id.id),
  581. ('company_id', '=', False),
  582. ],
  583. )
  584. for domain in domains:
  585. partner = self.env['res.partner'].search(list(domain) + [('parent_id', '=', False)], limit=1)
  586. if partner:
  587. return partner
  588. # Retrieve the partner from the 'reconcile models'.
  589. rec_models = self.env['account.reconcile.model'].search([
  590. ('rule_type', '!=', 'writeoff_button'),
  591. ('company_id', '=', self.company_id.id),
  592. ])
  593. for rec_model in rec_models:
  594. partner = rec_model._get_partner_from_mapping(self)
  595. if partner and rec_model._is_applicable_for(self, partner):
  596. return partner
  597. return self.env['res.partner']
  598. def _seek_for_lines(self):
  599. """ Helper used to dispatch the journal items between:
  600. - The lines using the liquidity account.
  601. - The lines using the transfer account.
  602. - The lines being not in one of the two previous categories.
  603. :return: (liquidity_lines, suspense_lines, other_lines)
  604. """
  605. liquidity_lines = self.env['account.move.line']
  606. suspense_lines = self.env['account.move.line']
  607. other_lines = self.env['account.move.line']
  608. for line in self.move_id.line_ids:
  609. if line.account_id == self.journal_id.default_account_id:
  610. liquidity_lines += line
  611. elif line.account_id == self.journal_id.suspense_account_id:
  612. suspense_lines += line
  613. else:
  614. other_lines += line
  615. if not liquidity_lines:
  616. liquidity_lines = self.move_id.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_cash')
  617. other_lines -= liquidity_lines
  618. return liquidity_lines, suspense_lines, other_lines
  619. # SYNCHRONIZATION account.bank.statement.line <-> account.move
  620. # -------------------------------------------------------------------------
  621. def _synchronize_from_moves(self, changed_fields):
  622. """ Update the account.bank.statement.line regarding its related account.move.
  623. Also, check both models are still consistent.
  624. :param changed_fields: A set containing all modified fields on account.move.
  625. """
  626. if self._context.get('skip_account_move_synchronization'):
  627. return
  628. for st_line in self.with_context(skip_account_move_synchronization=True):
  629. move = st_line.move_id
  630. move_vals_to_write = {}
  631. st_line_vals_to_write = {}
  632. if 'line_ids' in changed_fields:
  633. liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines()
  634. company_currency = st_line.journal_id.company_id.currency_id
  635. journal_currency = st_line.journal_id.currency_id if st_line.journal_id.currency_id != company_currency\
  636. else False
  637. if len(liquidity_lines) != 1:
  638. raise UserError(_(
  639. "The journal entry %s reached an invalid state regarding its related statement line.\n"
  640. "To be consistent, the journal entry must always have exactly one journal item involving the "
  641. "bank/cash account."
  642. ) % st_line.move_id.display_name)
  643. st_line_vals_to_write.update({
  644. 'payment_ref': liquidity_lines.name,
  645. 'partner_id': liquidity_lines.partner_id.id,
  646. })
  647. # Update 'amount' according to the liquidity line.
  648. if journal_currency:
  649. st_line_vals_to_write.update({
  650. 'amount': liquidity_lines.amount_currency,
  651. })
  652. else:
  653. st_line_vals_to_write.update({
  654. 'amount': liquidity_lines.balance,
  655. })
  656. if len(suspense_lines) > 1:
  657. raise UserError(_(
  658. "%s reached an invalid state regarding its related statement line.\n"
  659. "To be consistent, the journal entry must always have exactly one suspense line.", st_line.move_id.display_name
  660. ))
  661. elif len(suspense_lines) == 1:
  662. if journal_currency and suspense_lines.currency_id == journal_currency:
  663. # The suspense line is expressed in the journal's currency meaning the foreign currency
  664. # set on the statement line is no longer needed.
  665. st_line_vals_to_write.update({
  666. 'amount_currency': 0.0,
  667. 'foreign_currency_id': False,
  668. })
  669. elif not journal_currency and suspense_lines.currency_id == company_currency:
  670. # Don't set a specific foreign currency on the statement line.
  671. st_line_vals_to_write.update({
  672. 'amount_currency': 0.0,
  673. 'foreign_currency_id': False,
  674. })
  675. else:
  676. # Update the statement line regarding the foreign currency of the suspense line.
  677. st_line_vals_to_write.update({
  678. 'amount_currency': -suspense_lines.amount_currency,
  679. 'foreign_currency_id': suspense_lines.currency_id.id,
  680. })
  681. move_vals_to_write.update({
  682. 'partner_id': liquidity_lines.partner_id.id,
  683. 'currency_id': (st_line.foreign_currency_id or journal_currency or company_currency).id,
  684. })
  685. move.write(move._cleanup_write_orm_values(move, move_vals_to_write))
  686. st_line.write(move._cleanup_write_orm_values(st_line, st_line_vals_to_write))
  687. def _synchronize_to_moves(self, changed_fields):
  688. """ Update the account.move regarding the modified account.bank.statement.line.
  689. :param changed_fields: A list containing all modified fields on account.bank.statement.line.
  690. """
  691. if self._context.get('skip_account_move_synchronization'):
  692. return
  693. if not any(field_name in changed_fields for field_name in (
  694. 'payment_ref', 'amount', 'amount_currency',
  695. 'foreign_currency_id', 'currency_id', 'partner_id',
  696. )):
  697. return
  698. for st_line in self.with_context(skip_account_move_synchronization=True):
  699. liquidity_lines, suspense_lines, other_lines = st_line._seek_for_lines()
  700. journal = st_line.journal_id
  701. company_currency = journal.company_id.currency_id
  702. journal_currency = journal.currency_id if journal.currency_id != company_currency else False
  703. line_vals_list = st_line._prepare_move_line_default_vals()
  704. line_ids_commands = [(1, liquidity_lines.id, line_vals_list[0])]
  705. if suspense_lines:
  706. line_ids_commands.append((1, suspense_lines.id, line_vals_list[1]))
  707. else:
  708. line_ids_commands.append((0, 0, line_vals_list[1]))
  709. for line in other_lines:
  710. line_ids_commands.append((2, line.id))
  711. st_line_vals = {
  712. 'currency_id': (st_line.foreign_currency_id or journal_currency or company_currency).id,
  713. 'line_ids': line_ids_commands,
  714. }
  715. if st_line.move_id.journal_id != journal:
  716. st_line_vals['journal_id'] = journal.id
  717. if st_line.move_id.partner_id != st_line.partner_id:
  718. st_line_vals['partner_id'] = st_line.partner_id.id
  719. st_line.move_id.write(st_line_vals)
  720. # For optimization purpose, creating the reverse relation of m2o in _inherits saves
  721. # a lot of SQL queries
  722. class AccountMove(models.Model):
  723. _name = "account.move"
  724. _inherit = ['account.move']
  725. statement_line_ids = fields.One2many('account.bank.statement.line', 'move_id', string='Statements')