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