Files

1318 lines
47 KiB
Python
Raw Permalink Normal View History

2014-12-02 12:54:50 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2021-12-11 17:58:41 +01:00
from collections import defaultdict, namedtuple
2010-08-16 19:23:32 +02:00
from decimal import Decimal
2014-07-20 13:13:00 +02:00
from itertools import groupby
from sql import Null
2015-03-02 23:36:17 +01:00
from sql.aggregate import Max, Sum
2021-12-11 17:58:41 +01:00
from sql.conditionals import Coalesce
2023-08-03 15:53:18 +02:00
from sql.functions import CharLength
from sql.operators import Concat
2014-07-20 13:13:00 +02:00
2025-10-07 08:19:57 +02:00
import trytond.config as config
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2021-12-11 17:58:41 +01:00
from trytond.model import (
2025-12-02 16:01:39 +01:00
ChatMixin, Check, DictSchemaMixin, Index, ModelSQL, ModelView, Workflow,
fields, sequence_ordered)
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2014-07-20 13:13:00 +02:00
from trytond.modules.company import CompanyReport
2021-08-21 09:09:55 +02:00
from trytond.modules.currency.fields import Monetary
2021-12-11 17:58:41 +01:00
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If
from trytond.rpc import RPC
from trytond.transaction import Transaction
from trytond.wizard import Button, StateAction, StateView, Wizard
2008-06-17 12:09:01 +02:00
2018-12-29 14:20:29 +01:00
from .exceptions import (
2021-12-11 17:58:41 +01:00
StatementPostError, StatementValidateError, StatementValidateWarning)
2018-12-29 14:20:29 +01:00
if config.getboolean('account_statement', 'filestore', default=False):
file_id = 'origin_file_id'
store_prefix = config.get(
'account_statement', 'store_prefix', default=None)
else:
file_id = None
store_prefix = None
2008-06-17 12:09:01 +02:00
class Unequal(object):
2014-07-20 13:13:00 +02:00
"Always different"
def __eq__(self, other):
return False
def __ne__(self, other):
return True
def __str__(self):
return ''
2025-12-02 16:01:39 +01:00
class Statement(Workflow, ModelSQL, ModelView, ChatMixin):
2012-09-24 22:17:36 +02:00
__name__ = 'account.statement'
_states = {'readonly': Eval('state') != 'draft'}
_balance_states = _states.copy()
_balance_states.update({
'invisible': ~Eval('validation', '').in_(['balance']),
'required': Eval('validation', '').in_(['balance']),
})
_amount_states = _states.copy()
_amount_states.update({
'invisible': ~Eval('validation', '').in_(['amount']),
'required': Eval('validation', '').in_(['amount']),
})
_number_states = _states.copy()
_number_states.update({
'invisible': ~Eval('validation', '').in_(['number_of_lines']),
'required': Eval('validation', '').in_(['number_of_lines']),
})
2014-07-20 13:45:45 +02:00
name = fields.Char('Name', required=True)
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company", required=True,
states={
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
})
journal = fields.Many2One(
'account.statement.journal', "Journal", required=True,
2011-04-10 20:59:25 +02:00
domain=[
2014-12-03 16:13:25 +01:00
('company', '=', Eval('company', -1)),
2011-08-04 16:57:19 +02:00
],
2011-04-10 20:59:25 +02:00
states={
2013-12-18 23:15:29 +01:00
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
})
2020-10-09 19:09:05 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"), 'on_change_with_currency')
date = fields.Date("Date", required=True)
2021-08-21 09:09:55 +02:00
start_balance = Monetary(
"Start Balance", currency='currency', digits='currency',
states=_balance_states)
2021-08-21 09:09:55 +02:00
end_balance = Monetary(
"End Balance", currency='currency', digits='currency',
states=_balance_states)
2021-08-21 09:09:55 +02:00
balance = fields.Function(Monetary(
"Balance", currency='currency', digits='currency',
states=_balance_states),
2012-09-24 22:17:36 +02:00
'on_change_with_balance')
2021-08-21 09:09:55 +02:00
total_amount = Monetary(
"Total Amount", currency='currency', digits='currency',
states=_amount_states)
2014-07-20 13:45:45 +02:00
number_of_lines = fields.Integer('Number of Lines',
states=_number_states)
lines = fields.One2Many('account.statement.line', 'statement',
2014-07-10 00:08:44 +02:00
'Lines', states={
'readonly': (
(Eval('state') != 'draft')
| ~Eval('journal')
| Eval('origins', [])),
})
origins = fields.One2Many('account.statement.origin', 'statement',
"Origins", states={
'readonly': Eval('state') != 'draft',
})
origin_file = fields.Binary(
"Origin File", readonly=True,
file_id=file_id, store_prefix=store_prefix)
origin_file_id = fields.Char("Origin File ID", readonly=True)
state = fields.Selection([
('draft', "Draft"),
('validated', "Validated"),
('cancelled', "Cancelled"),
('posted', "Posted"),
], "State", readonly=True, sort=False)
2014-07-20 13:45:45 +02:00
validation = fields.Function(fields.Char('Validation'),
'on_change_with_validation')
2017-12-26 17:41:30 +01:00
to_reconcile = fields.Function(
fields.Boolean("To Reconcile"), 'get_to_reconcile')
2008-06-10 22:01:27 +02:00
del _states
del _balance_states
del _amount_states
del _number_states
2012-09-24 22:17:36 +02:00
@classmethod
def __setup__(cls):
2025-04-04 00:45:18 +02:00
super().__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(
t,
2024-07-27 18:36:14 +02:00
(t.journal, Index.Range()),
(t.date, Index.Range(order='DESC'))),
Index(
t,
2024-07-27 18:36:14 +02:00
(t.state, Index.Equality(cardinality='low')),
where=t.state.in_(['draft', 'validated'])),
})
2012-09-24 22:17:36 +02:00
cls._order[0] = ('id', 'DESC')
cls._transitions |= set((
2012-03-25 00:17:31 +01:00
('draft', 'validated'),
('draft', 'cancelled'),
2012-03-25 00:17:31 +01:00
('validated', 'posted'),
('validated', 'cancelled'),
('cancelled', 'draft'),
2012-03-25 00:17:31 +01:00
))
2012-09-24 22:17:36 +02:00
cls._buttons.update({
2012-03-25 00:17:31 +01:00
'draft': {
'invisible': Eval('state') != 'cancelled',
2018-02-19 11:38:12 +01:00
'depends': ['state'],
2012-03-25 00:17:31 +01:00
},
'validate_statement': {
2012-03-25 00:17:31 +01:00
'invisible': Eval('state') != 'draft',
2018-02-19 11:38:12 +01:00
'depends': ['state'],
2012-03-25 00:17:31 +01:00
},
'post': {
'invisible': Eval('state') != 'validated',
2018-02-19 11:38:12 +01:00
'depends': ['state'],
2012-03-25 00:17:31 +01:00
},
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'validated']),
2018-02-19 11:38:12 +01:00
'depends': ['state'],
2012-03-25 00:17:31 +01:00
},
2017-12-26 17:41:30 +01:00
'reconcile': {
'invisible': Eval('state').in_(['draft', 'cancelled']),
2017-12-26 17:41:30 +01:00
'readonly': ~Eval('to_reconcile'),
2018-02-19 11:38:12 +01:00
'depends': ['state', 'to_reconcile'],
2017-12-26 17:41:30 +01:00
},
2012-03-25 00:17:31 +01:00
})
2019-10-11 20:45:22 +02:00
cls.__rpc__.update({
'post': RPC(
readonly=False, instantiate=0, fresh_session=True),
})
2008-06-10 22:01:27 +02:00
2012-09-24 22:17:36 +02:00
@staticmethod
def default_company():
2012-03-27 10:43:12 +02:00
return Transaction().context.get('company')
2011-04-10 20:59:25 +02:00
2012-09-24 22:17:36 +02:00
@staticmethod
def default_state():
2008-06-10 22:01:27 +02:00
return 'draft'
2012-09-24 22:17:36 +02:00
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
2008-07-30 16:49:16 +02:00
@fields.depends('company', 'journal', 'date')
def on_change_with_start_balance(self):
if (all([self.company, self.journal, self.date])
and self.journal.validation == 'balance'):
if statements := self.search([
('company', '=', self.company.id),
('journal', '=', self.journal.id),
('date', '<=', self.date),
],
order=[
('date', 'DESC'),
('id', 'DESC'),
],
limit=1):
statement, = statements
return statement.end_balance
2008-06-10 22:01:27 +02:00
2020-10-09 19:09:05 +02:00
@fields.depends('journal')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.journal.currency if self.journal else None
2020-10-09 19:09:05 +02:00
2014-01-26 14:28:55 +01:00
@fields.depends('start_balance', 'end_balance')
2012-09-24 22:17:36 +02:00
def on_change_with_balance(self, name=None):
return ((getattr(self, 'end_balance', 0) or 0)
- (getattr(self, 'start_balance', 0) or 0))
@fields.depends('origins', 'lines', 'journal', 'company')
def on_change_origins(self):
invoices = {l.invoice for l in self.lines if l.invoice}
invoices.update(
l.invoice for o in self.origins for l in o.lines if l.invoice)
invoice_id2amount_to_pay = {}
for invoice in invoices:
if invoice.type == 'out':
sign = -1
else:
sign = 1
invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay
origins = list(self.origins)
for origin in origins:
lines = list(origin.lines)
for line in lines:
if (line.invoice
and line.id
and line.invoice.id in invoice_id2amount_to_pay):
amount_to_pay = invoice_id2amount_to_pay[line.invoice.id]
if (amount_to_pay
and getattr(line, 'amount', None)
and (line.amount >= 0) == (amount_to_pay <= 0)):
if abs(line.amount) > abs(amount_to_pay):
line.amount = amount_to_pay.copy_sign(line.amount)
else:
invoice_id2amount_to_pay[line.invoice.id] = (
line.amount + amount_to_pay)
else:
line.invoice = None
origin.lines = lines
self.origins = origins
@fields.depends('lines', 'origins')
2012-09-24 22:17:36 +02:00
def on_change_lines(self):
2011-07-03 13:18:45 +02:00
pool = Pool()
2012-09-24 22:17:36 +02:00
Line = pool.get('account.statement.line')
if self.origins:
# let origin manage the pending amounts
return
invoices = {l.invoice for l in self.lines if l.invoice}
invoice_id2amount_to_pay = {}
for invoice in invoices:
if invoice.type == 'out':
sign = -1
else:
sign = 1
invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay
lines = list(self.lines)
line_offset = 0
for index, line in enumerate(self.lines or []):
if line.invoice and line.id:
if line.invoice.id not in invoice_id2amount_to_pay:
continue
if getattr(line, 'amount', None) is None:
continue
amount_to_pay = invoice_id2amount_to_pay[line.invoice.id]
if ((line.amount > 0) == (amount_to_pay < 0)
or not amount_to_pay):
if abs(line.amount) > abs(amount_to_pay):
new_line = Line()
2018-06-26 19:11:43 +02:00
for field_name, field in Line._fields.items():
if field_name == 'id':
continue
try:
setattr(new_line, field_name,
getattr(line, field_name))
except AttributeError:
pass
new_line.amount = line.amount + amount_to_pay
# As the line is overpaying an invoice we can assume
# that the additional line will be used to pay an
# invoice
new_line.related_to = ('account.invoice', -1)
line_offset += 1
lines.insert(index + line_offset, new_line)
2024-02-12 23:45:14 +01:00
invoice_id2amount_to_pay[line.invoice.id] = Decimal(0)
line.amount = amount_to_pay.copy_sign(line.amount)
else:
invoice_id2amount_to_pay[line.invoice.id] = (
line.amount + amount_to_pay)
else:
line.invoice = None
self.lines = lines
2014-07-20 13:45:45 +02:00
@fields.depends('journal')
def on_change_with_validation(self, name=None):
if self.journal:
return self.journal.validation
2017-12-26 17:41:30 +01:00
def get_to_reconcile(self, name=None):
return bool(self.lines_to_reconcile)
@property
def lines_to_reconcile(self):
lines = []
for line in self.lines:
if line.move:
for move_line in line.move.lines:
if (move_line.account.reconcile
and not move_line.reconciliation):
lines.append(move_line)
return lines
2014-07-20 13:13:00 +02:00
def _group_key(self, line):
key = (
('number', line.number or Unequal()),
2014-07-20 13:13:00 +02:00
('date', line.date),
('party', line.party),
)
return key
def _get_grouped_line(self):
"Return Line class for grouped lines"
2018-10-15 22:54:19 +02:00
lines = self.origins or self.lines
assert lines
2014-07-20 13:13:00 +02:00
2018-10-15 22:54:19 +02:00
keys = [k[0] for k in self._group_key(lines[0])]
2014-07-20 13:13:00 +02:00
class Line(namedtuple('Line', keys + ['lines'])):
@property
def amount(self):
2019-04-23 12:03:21 +02:00
return sum((l.amount for l in self.lines))
2014-07-20 13:13:00 +02:00
@property
def descriptions(self):
done = set()
2019-04-23 12:03:21 +02:00
for line in self.lines:
2014-07-20 13:13:00 +02:00
if line.description and line.description not in done:
done.add(line.description)
yield line.description
return Line
@property
def grouped_lines(self):
if self.origins:
2018-10-15 22:54:19 +02:00
lines = self.origins
elif self.lines:
2018-10-15 22:54:19 +02:00
lines = self.lines
else:
return
Line = self._get_grouped_line()
for key, lines in groupby(lines, key=self._group_key):
yield Line(**dict(key + (('lines', list(lines)),)))
2014-07-20 13:13:00 +02:00
2019-09-20 11:47:15 +02:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
return super().view_attributes() + [
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
2019-09-20 11:47:15 +02:00
]
2012-09-24 22:17:36 +02:00
@classmethod
def check_modification(cls, mode, statements, values=None, external=False):
super().check_modification(
mode, statements, values=values, external=external)
if mode == 'delete':
for statement in statements:
if statement.state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'account_statement.msg_statement_delete_cancel',
statement=statement.rec_name))
2012-09-24 22:17:36 +02:00
@classmethod
2012-03-25 00:17:31 +01:00
@ModelView.button
@Workflow.transition('draft')
2012-09-24 22:17:36 +02:00
def draft(cls, statements):
2012-03-25 00:17:31 +01:00
pass
2014-07-20 13:45:45 +02:00
def validate_balance(self):
pool = Pool()
Lang = pool.get('ir.lang')
2018-12-29 14:20:29 +01:00
amount = (self.start_balance
2014-07-20 13:45:45 +02:00
+ sum(l.amount for l in self.lines))
2018-12-29 14:20:29 +01:00
if amount != self.end_balance:
2017-11-15 00:18:10 +01:00
lang = Lang.get()
2018-12-29 14:20:29 +01:00
end_balance = lang.currency(
self.end_balance, self.journal.currency)
amount = lang.currency(amount, self.journal.currency)
raise StatementValidateError(
gettext('account_statement.msg_statement_wrong_end_balance',
statement=self.rec_name,
end_balance=end_balance,
amount=amount))
2014-07-20 13:45:45 +02:00
def validate_amount(self):
pool = Pool()
Lang = pool.get('ir.lang')
amount = sum(l.amount for l in self.lines)
if amount != self.total_amount:
2017-11-15 00:18:10 +01:00
lang = Lang.get()
2018-12-29 14:20:29 +01:00
total_amount = lang.currency(
self.total_amount, self.journal.currency)
2017-11-15 00:18:10 +01:00
amount = lang.currency(amount, self.journal.currency)
2018-12-29 14:20:29 +01:00
raise StatementValidateError(
gettext('account_statement.msg_statement_wrong_total_amount',
statement=self.rec_name,
total_amount=total_amount,
amount=amount))
2014-07-20 13:45:45 +02:00
def validate_number_of_lines(self):
number = len(list(self.grouped_lines))
2018-12-29 14:20:29 +01:00
if number > self.number_of_lines:
raise StatementValidateError(
gettext('account_statement'
'.msg_statement_wrong_number_of_lines_remove',
statement=self.rec_name,
n=number - self.number_of_lines))
elif number < self.number_of_lines:
raise StatementValidateError(
gettext('account_statement'
'.msg_statement_wrong_number_of_lines_remove',
statement=self.rec_name,
n=self.number_of_lines - number))
2014-07-20 13:45:45 +02:00
2012-09-24 22:17:36 +02:00
@classmethod
2012-03-25 00:17:31 +01:00
@ModelView.button
@Workflow.transition('validated')
def validate_statement(cls, statements):
pool = Pool()
Line = pool.get('account.statement.line')
2018-12-29 14:20:29 +01:00
Warning = pool.get('res.user.warning')
paid_cancelled_invoice_lines = []
2012-09-24 22:17:36 +02:00
for statement in statements:
2014-07-20 13:45:45 +02:00
getattr(statement, 'validate_%s' % statement.validation)()
paid_cancelled_invoice_lines.extend(l for l in statement.lines
if l.invoice and l.invoice.state in {'cancelled', 'paid'})
if paid_cancelled_invoice_lines:
warning_key = Warning.format(
'statement_paid_cancelled_invoice_lines',
paid_cancelled_invoice_lines)
if Warning.check(warning_key):
raise StatementValidateWarning(warning_key,
gettext('account_statement'
'.msg_statement_invoice_paid_cancelled'))
Line.write(paid_cancelled_invoice_lines, {
'related_to': None,
})
2015-01-02 15:43:29 +01:00
cls.create_move(statements)
cls.write(statements, {
'state': 'validated',
})
common_lines = [l for l in Line.search([
('statement.state', '=', 'draft'),
('related_to.state', 'in', ['posted', 'paid'],
'account.invoice'),
])
if l.invoice.reconciled]
if common_lines:
warning_key = '_'.join(str(l.id) for l in common_lines)
2018-12-29 14:20:29 +01:00
if Warning.check(warning_key):
raise StatementValidateWarning(warning_key,
gettext('account_statement'
'.msg_statement_paid_invoice_draft'))
Line.write(common_lines, {
'related_to': None,
})
2008-06-10 22:01:27 +02:00
2015-01-02 15:43:29 +01:00
@classmethod
def create_move(cls, statements):
2015-01-07 14:51:34 +01:00
'''Create move for the statements and try to reconcile the lines.
Returns the list of move, statement and lines
'''
2015-01-02 15:43:29 +01:00
pool = Pool()
Line = pool.get('account.statement.line')
Move = pool.get('account.move')
MoveLine = pool.get('account.move.line')
moves = []
for statement in statements:
if statement.origins:
origin_index = {
o: index for index, o in enumerate(statement.origins)}
size = len(statement.origins)
sorted_lines = sorted(
statement.lines,
key=lambda l: origin_index.get(l.origin, size))
else:
sorted_lines = statement.lines
for key, lines in groupby(sorted_lines, key=statement._group_key):
2015-01-02 15:43:29 +01:00
lines = list(lines)
key = dict(key)
move = statement._get_move(key)
moves.append((move, statement, lines))
Move.save([m for m, _, _ in moves])
to_write = []
for move, _, lines in moves:
to_write.append(lines)
to_write.append({
'move': move.id,
})
if to_write:
Line.write(*to_write)
2015-01-02 15:43:29 +01:00
move_lines = []
for move, statement, lines in moves:
amount = 0
amount_second_currency = 0
for line in lines:
move_line = line.get_move_line()
2023-02-21 12:37:07 +01:00
if not move_line:
continue
2015-01-02 15:43:29 +01:00
move_line.move = move
amount += move_line.debit - move_line.credit
if move_line.amount_second_currency:
amount_second_currency += move_line.amount_second_currency
move_lines.append((move_line, line))
move_line = statement._get_move_line(
2017-02-01 21:43:20 +01:00
amount, amount_second_currency, lines)
2015-01-02 15:43:29 +01:00
move_line.move = move
move_lines.append((move_line, None))
MoveLine.save([l for l, _ in move_lines])
2018-03-07 00:11:08 +01:00
Line.reconcile(move_lines)
2015-01-07 14:51:34 +01:00
return moves
2015-01-02 15:43:29 +01:00
def _get_move(self, key):
'Return Move for the grouping key'
pool = Pool()
Move = pool.get('account.move')
Period = pool.get('account.period')
period = Period.find(self.company, date=key['date'])
2015-01-02 15:43:29 +01:00
return Move(
period=period,
2015-01-02 15:43:29 +01:00
journal=self.journal.journal,
date=key['date'],
origin=self,
company=self.company,
)
2017-02-01 21:43:20 +01:00
def _get_move_line(self, amount, amount_second_currency, lines):
2015-01-02 15:43:29 +01:00
'Return counterpart Move Line for the amount'
pool = Pool()
MoveLine = pool.get('account.move.line')
if self.journal.currency != self.company.currency:
second_currency = self.journal.currency
amount_second_currency *= -1
2015-01-02 15:43:29 +01:00
else:
second_currency = None
amount_second_currency = None
return MoveLine(
debit=abs(amount) if amount < 0 else 0,
credit=abs(amount) if amount > 0 else 0,
2018-09-01 11:19:35 +02:00
account=self.journal.account,
2015-01-02 15:43:29 +01:00
second_currency=second_currency,
amount_second_currency=amount_second_currency,
)
2012-09-24 22:17:36 +02:00
@classmethod
2012-03-25 00:17:31 +01:00
@ModelView.button
@Workflow.transition('posted')
2012-09-24 22:17:36 +02:00
def post(cls, statements):
2018-12-29 14:20:29 +01:00
pool = Pool()
Lang = pool.get('ir.lang')
StatementLine = pool.get('account.statement.line')
for statement in statements:
for origin in statement.origins:
if origin.pending_amount:
2018-12-29 14:20:29 +01:00
lang = Lang.get()
amount = lang.currency(
origin.pending_amount, statement.journal.currency)
raise StatementPostError(
gettext('account_statement'
'.msg_statement_post_pending_amount',
statement=statement.rec_name,
amount=amount,
origin=origin.rec_name))
# Write state to skip statement test on Move.post
cls.write(statements, {'state': 'posted'})
2012-03-25 00:17:31 +01:00
lines = [l for s in statements for l in s.lines]
2012-09-24 22:17:36 +02:00
StatementLine.post_move(lines)
2008-06-10 22:01:27 +02:00
2012-09-24 22:17:36 +02:00
@classmethod
2012-03-25 00:17:31 +01:00
@ModelView.button
@Workflow.transition('cancelled')
2012-09-24 22:17:36 +02:00
def cancel(cls, statements):
StatementLine = Pool().get('account.statement.line')
2012-03-25 00:17:31 +01:00
lines = [l for s in statements for l in s.lines]
2012-09-24 22:17:36 +02:00
StatementLine.delete_move(lines)
2008-06-10 22:01:27 +02:00
2017-12-26 17:41:30 +01:00
@classmethod
@ModelView.button_action('account_statement.act_reconcile')
def reconcile(cls, statements):
pass
2022-01-30 13:16:08 +01:00
@classmethod
def copy(cls, statements, default=None):
default = default.copy() if default is not None else {}
new_statements = []
for origins, sub_statements in groupby(
statements, key=lambda s: bool(s.origins)):
sub_statements = list(sub_statements)
sub_default = default.copy()
if origins:
sub_default.setdefault('lines')
new_statements.extend(super().copy(
statements, default=sub_default))
return new_statements
2008-06-17 12:09:01 +02:00
def origin_mixin(_states):
class Mixin:
2019-09-07 23:45:08 +02:00
__slots__ = ()
statement = fields.Many2One(
'account.statement', "Statement",
required=True, ondelete='CASCADE', states=_states)
statement_state = fields.Function(
fields.Selection('get_statement_states', "Statement State"),
'on_change_with_statement_state')
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company', searcher='search_company')
2023-10-30 17:37:54 +01:00
company_currency = fields.Function(
fields.Many2One('currency.currency', "Company Currency"),
'on_change_with_company_currency')
number = fields.Char("Number", states=_states)
date = fields.Date(
"Date", required=True, states=_states)
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency', required=True,
states=_states)
2020-10-09 19:09:05 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"), 'on_change_with_currency')
2023-10-30 17:37:54 +01:00
amount_second_currency = Monetary(
"Amount Second Currency",
currency='second_currency', digits='second_currency',
states={
'required': Bool(Eval('second_currency')),
'readonly': _states['readonly'],
})
second_currency = fields.Many2One(
'currency.currency', "Second Currency",
domain=[
('id', '!=', Eval('currency', -1)),
If(Eval('currency', -1) != Eval('company_currency', -1),
('id', '=', Eval('company_currency', -1)),
()),
],
states={
'required': Bool(Eval('amount_second_currency')),
'readonly': _states['readonly'],
})
party = fields.Many2One(
'party.party', "Party", states=_states,
context={
'company': Eval('company', -1),
},
depends={'company'})
account = fields.Many2One(
'account.account', "Account",
domain=[
('company', '=', Eval('company', -1)),
2019-03-24 18:00:01 +01:00
('type', '!=', None),
('closed', '!=', True),
],
2018-08-31 14:58:05 +02:00
context={
'date': Eval('date'),
},
states=_states, depends={'date'})
description = fields.Char("Description", states=_states)
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
2021-03-07 11:13:53 +01:00
super().__setup__()
cls.__access__.add('statement')
2023-08-03 15:53:18 +02:00
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
@classmethod
def get_statement_states(cls):
pool = Pool()
Statement = pool.get('account.statement')
return Statement.fields_get(['state'])['state']['selection']
@fields.depends('statement', '_parent_statement.state')
def on_change_with_statement_state(self, name=None):
if self.statement:
return self.statement.state
@fields.depends('statement', '_parent_statement.company')
def on_change_with_company(self, name=None):
2023-03-26 09:49:00 +02:00
return self.statement.company if self.statement else None
@classmethod
def search_company(cls, name, clause):
return [('statement.' + clause[0],) + tuple(clause[1:])]
2023-10-30 17:37:54 +01:00
@fields.depends('statement', '_parent_statement.company')
def on_change_with_company_currency(self, name=None):
return self.statement.company.currency if self.statement else None
2020-10-09 19:09:05 +02:00
@fields.depends('statement', '_parent_statement.journal')
def on_change_with_currency(self, name=None):
if self.statement and self.statement.journal:
2023-03-26 09:49:00 +02:00
return self.statement.journal.currency
2020-10-09 19:09:05 +02:00
return Mixin
_states = {
'readonly': Eval('statement_state') != 'draft',
}
class Line(origin_mixin(_states), sequence_ordered(), ModelSQL, ModelView):
2012-09-24 22:17:36 +02:00
__name__ = 'account.statement.line'
2016-06-30 18:55:57 +02:00
2014-12-03 16:13:25 +01:00
move = fields.Many2One('account.move', 'Account Move', readonly=True,
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
related_to = fields.Reference(
"Related To", 'get_relations',
domain={
'account.invoice': [
('company', '=', Eval('company', -1)),
2023-10-30 17:37:54 +01:00
If(Eval('second_currency'),
('currency', '=', Eval('second_currency', -1)),
('currency', '=', Eval('currency', -1))
),
If(Bool(Eval('party')),
2022-09-11 17:46:09 +02:00
['OR',
('party', '=', Eval('party', -1)),
('alternative_payees', '=', Eval('party', -1)),
],
[]),
If(Bool(Eval('account')),
('account', '=', Eval('account', -1)),
()),
If(Eval('statement_state') == 'draft',
('state', '=', 'posted'),
('state', '!=', '')),
],
},
2016-06-30 18:55:57 +02:00
states=_states,
context={'with_payment': False})
origin = fields.Many2One(
'account.statement.origin', 'Origin', readonly=True,
ondelete='RESTRICT',
states={
'invisible': ~Bool(Eval('origin')),
},
domain=[
2021-10-03 13:45:32 +02:00
('statement', '=', Eval('statement', -1)),
('date', '=', Eval('date', None)),
])
party_required = fields.Function(
fields.Boolean("Party Required"), 'on_change_with_party_required')
2008-06-10 22:01:27 +02:00
2012-09-24 22:17:36 +02:00
@classmethod
def __setup__(cls):
2025-04-04 00:45:18 +02:00
super().__setup__()
2023-10-30 17:37:54 +01:00
table = cls.__table__()
cls.date.states['readonly'] = (
cls.date.states.get('readonly', False)
| Bool(Eval('origin', 0)))
cls.account.required = True
cls.party.states['required'] = (
cls.party.states.get('required', False)
| (Eval('party_required', False)
& (Eval('statement_state') == 'draft')))
2024-10-27 16:20:45 +01:00
cls.amount_second_currency.domain = [
If(Eval('second_currency', None),
If(Eval('amount', 0) >= 0,
('amount_second_currency', '>=', 0),
('amount_second_currency', '<=', 0)),
()),
]
2023-10-30 17:37:54 +01:00
cls._sql_constraints += [
('second_currency_sign',
Check(
table,
Coalesce(table.amount_second_currency, 0)
* table.amount >= 0),
'account_statement.msg_statement_line_second_currency_sign'),
]
2008-07-17 19:36:26 +02:00
@classmethod
def __register__(cls, module):
table = cls.__table__()
super().__register__(module)
table_h = cls.__table_handler__(module)
cursor = Transaction().connection.cursor()
2022-05-07 11:12:25 +02:00
# Migration from 6.2: replace invoice by related_to
if table_h.column_exist('invoice'):
cursor.execute(*table.update(
[table.related_to],
[Concat('account.invoice,', table.invoice)],
where=table.invoice != Null))
table_h.drop_column('invoice')
2023-02-21 12:37:07 +01:00
# Migration from 6.6: Allow amount of zero
table_h.drop_constraint('check_statement_line_amount')
@property
@fields.depends('related_to')
def invoice(self):
pool = Pool()
Invoice = pool.get('account.invoice')
related_to = getattr(self, 'related_to', None)
if isinstance(related_to, Invoice) and related_to.id >= 0:
return related_to
@invoice.setter
def invoice(self, value):
self.related_to = value
@fields.depends('account')
def on_change_with_party_required(self, name=None):
if self.account:
return self.account.party_required
return False
2012-09-24 22:17:36 +02:00
@staticmethod
def default_amount():
2012-04-02 10:52:36 +02:00
return Decimal(0)
@classmethod
def _get_relations(cls):
"Return a list of Model names for related_to Reference"
return ['account.invoice']
@classmethod
def get_relations(cls):
Model = Pool().get('ir.model')
get_name = Model.get_name
models = cls._get_relations()
return [(None, '')] + [(m, get_name(m)) for m in models]
@fields.depends('amount', 'party', 'date', methods=['invoice'])
2012-09-24 22:17:36 +02:00
def on_change_party(self):
if self.party:
if self.amount:
2018-08-31 14:58:05 +02:00
with Transaction().set_context(date=self.date):
if self.amount > Decimal(0):
2018-08-31 14:58:05 +02:00
self.account = self.party.account_receivable_used
else:
self.account = self.party.account_payable_used
2012-09-24 22:17:36 +02:00
if self.invoice:
if self.party:
2022-09-11 17:46:09 +02:00
if (self.invoice.party != self.party
or self.party not in self.invoice.alternative_payees):
2014-10-29 20:39:23 +01:00
self.invoice = None
else:
2014-10-29 20:39:23 +01:00
self.invoice = None
2008-06-10 22:01:27 +02:00
@fields.depends(
'amount', 'party', 'account', 'date',
'statement', '_parent_statement.journal',
methods=['invoice'])
2012-09-24 22:17:36 +02:00
def on_change_amount(self):
if self.party:
2018-08-31 14:58:05 +02:00
with Transaction().set_context(date=self.date):
if self.account and self.account not in (
self.party.account_receivable_used,
self.party.account_payable_used):
# The user has entered a non-default value, we keep it.
pass
elif self.amount:
if self.amount > Decimal(0):
2018-08-31 14:58:05 +02:00
self.account = self.party.account_receivable_used
else:
self.account = self.party.account_payable_used
@fields.depends('account', methods=['invoice'])
2012-09-24 22:17:36 +02:00
def on_change_account(self):
if self.invoice:
if self.account:
if self.invoice.account != self.account:
2014-10-29 20:39:23 +01:00
self.invoice = None
else:
2014-10-29 20:39:23 +01:00
self.invoice = None
2008-06-17 18:33:07 +02:00
@fields.depends('party', 'account', methods=['invoice'])
def on_change_related_to(self):
if self.invoice:
if not self.party:
2022-09-11 17:46:09 +02:00
if not self.invoice.alternative_payees:
self.party = self.invoice.party
else:
try:
self.party, = self.invoice.alternative_payees
except ValueError:
pass
if not self.account:
2014-10-29 20:39:23 +01:00
self.account = self.invoice.account
@fields.depends('origin',
'_parent_origin.pending_amount', '_parent_origin.date',
'_parent_origin.party', '_parent_origin.account',
'_parent_origin.number', '_parent_origin.description',
'_parent_origin.statement',
2018-05-31 13:24:54 +02:00
methods=['on_change_party'])
def on_change_origin(self):
if self.origin:
self.amount = self.origin.pending_amount
self.date = self.origin.date
self.party = self.origin.party
self.number = self.origin.number
self.description = self.origin.description
self.statement = self.origin.statement
if self.origin.account:
self.account = self.origin.account
else:
self.on_change_party()
@fields.depends('origin', '_parent_origin.company')
def on_change_with_company(self, name=None):
try:
2023-03-26 09:49:00 +02:00
company = super().on_change_with_company()
except AttributeError:
company = None
2023-03-26 09:49:00 +02:00
if self.origin and hasattr(self.origin, 'company'):
company = self.origin.company
return company
@fields.depends('origin', '_parent_origin.statement_state')
def on_change_with_statement_state(self, name=None):
try:
2025-04-04 00:45:18 +02:00
state = super().on_change_with_statement_state()
except AttributeError:
state = None
if self.origin:
return self.origin.statement_state
return state
2012-09-29 15:34:37 +02:00
def get_rec_name(self, name):
return self.statement.rec_name
2013-11-25 12:31:02 +01:00
@classmethod
def search_rec_name(cls, name, clause):
return [('statement.rec_name',) + tuple(clause[1:])]
@classmethod
def check_modification(cls, mode, lines, values=None, external=False):
super().check_modification(
mode, lines, values=values, external=external)
if mode == 'delete':
for line in lines:
if line.statement_state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'account_statement.'
'msg_statement_line_delete_cancel_draft',
line=line.rec_name,
sale=line.statement.rec_name))
2012-09-24 22:17:36 +02:00
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('move', None)
default.setdefault('related_to', None)
2025-04-04 00:45:18 +02:00
return super().copy(lines, default=default)
2018-03-07 00:11:08 +01:00
@classmethod
def reconcile(cls, move_lines):
2011-07-03 13:18:45 +02:00
pool = Pool()
2015-01-02 15:43:29 +01:00
Invoice = pool.get('account.invoice')
MoveLine = pool.get('account.move.line')
2018-03-07 00:11:08 +01:00
invoice_payments = defaultdict(list)
to_reconcile = []
for move_line, line in move_lines:
if not line or not line.invoice:
continue
# Write previous invoice payments to have them when calling
# get_reconcile_lines_for_amount
if line.invoice in invoice_payments:
2018-03-24 15:30:29 +01:00
Invoice.add_payment_lines(invoice_payments)
invoice_payments.clear()
MoveLine.reconcile(*to_reconcile)
to_reconcile.clear()
2018-03-07 00:11:08 +01:00
if move_line.second_currency:
amount = -move_line.amount_second_currency
currency = move_line.second_currency
else:
amount = move_line.credit - move_line.debit
currency = line.company.currency
2018-03-07 00:11:08 +01:00
reconcile_lines = line.invoice.get_reconcile_lines_for_amount(
amount, currency, party=line.party)
2018-03-07 00:11:08 +01:00
assert move_line.account == line.invoice.account
2015-01-02 15:43:29 +01:00
2018-03-07 00:11:08 +01:00
invoice_payments[line.invoice].append(move_line.id)
if not reconcile_lines[1]:
to_reconcile.append(reconcile_lines[0] + [move_line])
if invoice_payments:
2018-03-24 15:30:29 +01:00
Invoice.add_payment_lines(invoice_payments)
2018-03-07 00:11:08 +01:00
if to_reconcile:
MoveLine.reconcile(*to_reconcile)
2008-06-17 19:21:23 +02:00
2012-09-24 22:17:36 +02:00
@classmethod
def post_move(cls, lines):
Move = Pool().get('account.move')
2019-12-05 21:38:03 +01:00
Move.post(list({l.move for l in lines
if l.move and l.move.state != 'posted'}))
2008-06-17 19:21:23 +02:00
2012-09-24 22:17:36 +02:00
@classmethod
def delete_move(cls, lines):
2017-09-03 11:23:02 +02:00
pool = Pool()
Move = pool.get('account.move')
Reconciliation = pool.get('account.move.reconciliation')
reconciliations = [l.reconciliation
for line in lines if line.move
for l in line.move.lines if l.reconciliation]
2017-09-03 11:23:02 +02:00
Reconciliation.delete(reconciliations)
2015-01-02 15:43:29 +01:00
Move.delete(list({l.move for l in lines if l.move}))
2008-07-25 12:51:24 +02:00
2015-01-02 15:43:29 +01:00
def get_move_line(self):
2008-09-24 17:43:00 +02:00
'''
2015-01-02 15:43:29 +01:00
Return the move line for the statement line
2008-09-24 17:43:00 +02:00
'''
2012-09-24 22:17:36 +02:00
pool = Pool()
MoveLine = pool.get('account.move.line')
Currency = Pool().get('currency.currency')
2023-02-21 12:37:07 +01:00
if not self.amount:
return
2023-10-30 17:37:54 +01:00
if self.second_currency == self.company_currency:
amount = self.amount_second_currency
else:
with Transaction().set_context(date=self.date):
amount = Currency.compute(
self.currency, self.amount, self.company_currency)
if self.currency != self.company_currency:
second_currency = self.currency
amount_second_currency = -self.amount
2023-10-30 17:37:54 +01:00
elif self.second_currency:
second_currency = self.second_currency
amount_second_currency = -self.amount_second_currency
else:
second_currency = None
2023-10-30 17:37:54 +01:00
amount_second_currency = None
2015-01-02 15:43:29 +01:00
return MoveLine(
2021-02-03 21:22:31 +01:00
origin=self,
2023-10-30 17:37:54 +01:00
debit=abs(amount) if amount < 0 else 0,
credit=abs(amount) if amount > 0 else 0,
2015-01-02 15:43:29 +01:00
account=self.account,
party=self.party if self.account.party_required else None,
second_currency=second_currency,
amount_second_currency=amount_second_currency,
)
2014-07-20 13:13:00 +02:00
2020-03-01 12:33:51 +01:00
del _states
2014-07-20 13:13:00 +02:00
2015-03-02 23:36:17 +01:00
class LineGroup(ModelSQL, ModelView):
__name__ = 'account.statement.line.group'
_rec_name = 'number'
statement = fields.Many2One(
'account.statement', 'Statement', required=True)
2015-03-02 23:36:17 +01:00
journal = fields.Function(fields.Many2One('account.statement.journal',
'Journal'), 'get_journal', searcher='search_journal')
number = fields.Char('Number')
date = fields.Date('Date')
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency')
2015-03-02 23:36:17 +01:00
currency = fields.Function(fields.Many2One('currency.currency',
'Currency'), 'get_currency')
2023-10-30 17:37:54 +01:00
amount_second_currency = Monetary(
"Amount Second Currency",
currency='second_currency', digits='second_currency')
second_currency = fields.Many2One('currency.currency', "Second Currency")
2015-03-02 23:36:17 +01:00
party = fields.Many2One('party.party', 'Party')
move = fields.Many2One('account.move', 'Move')
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
2025-04-04 00:45:18 +02:00
super().__setup__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('statement')
2015-03-02 23:36:17 +01:00
cls._order.insert(0, ('date', 'DESC'))
@classmethod
def _grouped_columns(cls, line):
return [
Max(line.statement).as_('statement'),
Max(line.number).as_('number'),
Max(line.date).as_('date'),
Sum(line.amount).as_('amount'),
2023-10-30 17:37:54 +01:00
Sum(line.amount_second_currency).as_('amount_second_currency'),
2015-03-02 23:36:17 +01:00
Max(line.party).as_('party'),
]
@classmethod
def table_query(cls):
pool = Pool()
Move = pool.get('account.move')
Line = pool.get('account.statement.line')
move = Move.__table__()
line = Line.__table__()
std_columns = [
move.id,
2023-10-30 17:37:54 +01:00
line.second_currency,
2015-03-02 23:36:17 +01:00
]
columns = (std_columns + [move.id.as_('move')]
+ cls._grouped_columns(line))
return move.join(line,
condition=move.id == line.move
).select(*columns,
2022-02-17 00:48:06 +01:00
where=move.origin.like(Statement.__name__ + ',%'),
2015-03-02 23:36:17 +01:00
group_by=std_columns + [move.id]
)
2023-08-03 15:53:18 +02:00
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
2015-03-02 23:36:17 +01:00
def get_journal(self, name):
return self.statement.journal.id
@classmethod
def search_journal(cls, name, clause):
2017-09-27 18:37:26 +02:00
return [('statement.' + clause[0],) + tuple(clause[1:])]
2015-03-02 23:36:17 +01:00
def get_currency(self, name):
return self.statement.journal.currency.id
_states = {
'readonly': (Eval('statement_state') != 'draft') | Eval('lines', []),
}
class Origin(origin_mixin(_states), ModelSQL, ModelView):
__name__ = 'account.statement.origin'
_rec_name = 'number'
lines = fields.One2Many(
'account.statement.line', 'origin', "Lines",
states={
2020-03-01 12:33:51 +01:00
'readonly': ((Eval('statement_id', -1) < 0)
| ~Eval('statement_state').in_(['draft', 'validated'])),
},
domain=[
2021-10-03 13:45:32 +02:00
('statement', '=', Eval('statement', -1)),
('date', '=', Eval('date', None)),
])
statement_id = fields.Function(
fields.Integer("Statement ID"), 'on_change_with_statement_id')
2021-08-21 09:09:55 +02:00
pending_amount = fields.Function(Monetary(
"Pending Amount", currency='currency', digits='currency'),
'on_change_with_pending_amount', searcher='search_pending_amount')
2019-02-06 22:34:13 +01:00
information = fields.Dict(
'account.statement.origin.information', "Information", readonly=True)
2019-10-16 21:18:55 +02:00
@fields.depends('statement', '_parent_statement.id')
def on_change_with_statement_id(self, name=None):
if self.statement:
return self.statement.id
return -1
@fields.depends('lines', 'amount')
def on_change_with_pending_amount(self, name=None):
lines_amount = sum(
getattr(l, 'amount') or Decimal(0) for l in self.lines)
return (self.amount or Decimal(0)) - lines_amount
@classmethod
def search_pending_amount(cls, name, clause):
pool = Pool()
Line = pool.get('account.statement.line')
table = cls.__table__()
line = Line.__table__()
_, operator, value = clause
Operator = fields.SQL_OPERATORS[operator]
query = (table.join(line, 'LEFT', condition=line.origin == table.id)
.select(table.id,
having=Operator(
table.amount - Coalesce(Sum(line.amount), 0), value),
group_by=table.id))
return [('id', 'in', query)]
2020-03-01 12:33:51 +01:00
@classmethod
def check_modification(cls, mode, origins, values=None, external=False):
super().check_modification(
mode, origins, values=values, external=external)
if mode == 'delete':
for origin in origins:
if origin.statement_state not in {'cancelled', 'draft'}:
raise AccessError(gettext(
'account_statement.'
'msg_statement_origin_delete_cancel_draft',
origin=origin.rec_name,
sale=origin.statement.rec_name))
2022-01-30 13:16:08 +01:00
@classmethod
def copy(cls, origins, default=None):
default = default.copy() if default is not None else {}
default.setdefault('lines')
return super().copy(origins, default=default)
2020-03-01 12:33:51 +01:00
del _states
class OriginInformation(DictSchemaMixin, ModelSQL, ModelView):
__name__ = 'account.statement.origin.information'
class ImportStatementStart(ModelView):
__name__ = 'account.statement.import.start'
company = fields.Many2One('company.company', "Company", required=True)
file_ = fields.Binary("File", required=True)
file_format = fields.Selection(
[(None, '')], "File Format", required=True, translate=False)
@classmethod
def default_file_format(cls):
return None
@classmethod
def default_company(cls):
return Transaction().context.get('company')
class ImportStatement(Wizard):
__name__ = 'account.statement.import'
start = StateView('account.statement.import.start',
'account_statement.statement_import_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Import", 'import_', 'tryton-ok', default=True),
])
import_ = StateAction('account_statement.act_statement_form')
def do_import_(self, action):
pool = Pool()
Statement = pool.get('account.statement')
statements = list(getattr(self, 'parse_%s' % self.start.file_format)())
for statement in statements:
statement.origin_file = fields.Binary.cast(self.start.file_)
Statement.save(statements)
self.start.file_ = None
2018-06-26 19:11:43 +02:00
data = {'res_id': list(map(int, statements))}
if len(statements) == 1:
action['views'].reverse()
return action, data
2017-12-26 17:41:30 +01:00
class ReconcileStatement(Wizard):
__name__ = 'account.statement.reconcile'
start = StateAction('account.act_reconcile')
def do_start(self, action):
lines = sum(
([int(l) for l in s.lines_to_reconcile] for s in self.records), [])
2017-12-26 17:41:30 +01:00
return action, {
'model': 'account.move.line',
'ids': lines,
}
2014-07-20 13:13:00 +02:00
class StatementReport(CompanyReport):
__name__ = 'account.statement'