Files
tryton/statement.py
T
Cédric Krier 5e8e0b1af9 Use lines attribute from grouped Line class
The lines from the scopes contain all the lines instead of only the grouped
lines.

issue8236
review263351002
2019-04-23 12:03:21 +02:00

1165 lines
41 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from collections import namedtuple, defaultdict
from itertools import groupby
from sql import Null
from sql.conditionals import Coalesce
from sql.aggregate import Max, Sum
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import Workflow, ModelView, ModelSQL, fields, Check, \
sequence_ordered, DictSchemaMixin
from trytond.model.exceptions import AccessError
from trytond.pyson import Eval, If, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.wizard import Wizard, StateView, StateAction, Button
from trytond.modules.company import CompanyReport
from .exceptions import (
StatementValidateError, StatementValidateWarning, StatementPostError)
__all__ = ['Statement', 'Line', 'LineGroup',
'Origin', 'OriginInformation',
'ImportStatementStart', 'ImportStatement',
'ReconcileStatement',
'StatementReport']
_STATES = {'readonly': Eval('state') != 'draft'}
_DEPENDS = ['state']
_BALANCE_STATES = _STATES.copy()
_BALANCE_STATES.update({
'invisible': ~Eval('validation', '').in_(['balance']),
'required': Eval('validation', '').in_(['balance']),
})
_BALANCE_DEPENDS = _DEPENDS + ['validation']
_AMOUNT_STATES = _STATES.copy()
_AMOUNT_STATES.update({
'invisible': ~Eval('validation', '').in_(['amount']),
'required': Eval('validation', '').in_(['amount']),
})
_AMOUNT_DEPENDS = _DEPENDS + ['validation']
_NUMBER_STATES = _STATES.copy()
_NUMBER_STATES.update({
'invisible': ~Eval('validation', '').in_(['number_of_lines']),
'required': Eval('validation', '').in_(['number_of_lines']),
})
_NUMBER_DEPENDS = _DEPENDS + ['validation']
STATES = [
('draft', 'Draft'),
('validated', 'Validated'),
('cancel', 'Canceled'),
('posted', 'Posted'),
]
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
class Unequal(object):
"Always different"
def __eq__(self, other):
return False
def __ne__(self, other):
return True
def __str__(self):
return ''
class Statement(Workflow, ModelSQL, ModelView):
'Account Statement'
__name__ = 'account.statement'
name = fields.Char('Name', required=True)
company = fields.Many2One('company.company', 'Company', required=True,
select=True, states=_STATES, domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', -1)),
],
depends=_DEPENDS)
journal = fields.Many2One('account.statement.journal', 'Journal',
required=True, select=True,
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
},
depends=['state', 'company'])
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
date = fields.Date('Date', required=True, select=True)
start_balance = fields.Numeric('Start Balance',
digits=(16, Eval('currency_digits', 2)),
states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits'])
end_balance = fields.Numeric('End Balance',
digits=(16, Eval('currency_digits', 2)),
states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits'])
balance = fields.Function(
fields.Numeric('Balance',
digits=(16, Eval('currency_digits', 2)),
states=_BALANCE_STATES,
depends=_BALANCE_DEPENDS + ['currency_digits']),
'on_change_with_balance')
total_amount = fields.Numeric('Total Amount',
digits=(16, Eval('currency_digits', 2)),
states=_AMOUNT_STATES, depends=_AMOUNT_DEPENDS + ['currency_digits'])
number_of_lines = fields.Integer('Number of Lines',
states=_NUMBER_STATES, depends=_NUMBER_DEPENDS)
lines = fields.One2Many('account.statement.line', 'statement',
'Lines', states={
'readonly': (Eval('state') != 'draft') | ~Eval('journal'),
},
depends=['state', 'journal'])
origins = fields.One2Many('account.statement.origin', 'statement',
"Origins", states={
'readonly': Eval('state') != 'draft',
},
depends=['state'])
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(STATES, 'State', readonly=True, select=True)
validation = fields.Function(fields.Char('Validation'),
'on_change_with_validation')
to_reconcile = fields.Function(
fields.Boolean("To Reconcile"), 'get_to_reconcile')
@classmethod
def __setup__(cls):
super(Statement, cls).__setup__()
cls._order[0] = ('id', 'DESC')
cls._transitions |= set((
('draft', 'validated'),
('draft', 'cancel'),
('validated', 'posted'),
('validated', 'cancel'),
('cancel', 'draft'),
))
cls._buttons.update({
'draft': {
'invisible': Eval('state') != 'cancel',
'depends': ['state'],
},
'validate_statement': {
'invisible': Eval('state') != 'draft',
'depends': ['state'],
},
'post': {
'invisible': Eval('state') != 'validated',
'depends': ['state'],
},
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'validated']),
'depends': ['state'],
},
'reconcile': {
'invisible': Eval('state').in_(['draft', 'cancel']),
'readonly': ~Eval('to_reconcile'),
'depends': ['state', 'to_reconcile'],
},
})
@classmethod
def __register__(cls, module_name):
transaction = Transaction()
cursor = transaction.connection.cursor()
sql_table = cls.__table__()
super(Statement, cls).__register__(module_name)
table = cls.__table_handler__(module_name)
# Migration from 3.2: remove required on start/end balance
table.not_null_action('start_balance', action='remove')
table.not_null_action('end_balance', action='remove')
# Migration from 3.2: add required name
cursor.execute(*sql_table.update([sql_table.name],
[sql_table.id.cast(cls.name.sql_type().base)],
where=sql_table.name == Null))
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_currency_digits():
Company = Pool().get('company.company')
if Transaction().context.get('company'):
company = Company(Transaction().context['company'])
return company.currency.digits
return 2
@fields.depends('journal', 'state', 'lines')
def on_change_journal(self):
if not self.journal:
return
statements = self.search([
('journal', '=', self.journal.id),
], order=[
('date', 'DESC'),
('id', 'DESC'),
], limit=1)
if not statements:
return
statement, = statements
self.start_balance = statement.end_balance
@fields.depends('journal')
def on_change_with_currency_digits(self, name=None):
if self.journal:
return self.journal.currency.digits
return 2
def get_end_balance(self, name):
end_balance = self.start_balance
for line in self.lines:
end_balance += line.amount
return end_balance
@fields.depends('start_balance', 'end_balance')
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):
if not self.journal or not self.origins or not self.company:
return
if self.journal.currency != self.company.currency:
return
invoices = set()
for line in self.lines:
if (getattr(line, 'invoice', None)
and line.invoice.currency == self.company.currency):
invoices.add(line.invoice)
for origin in self.origins:
for line in origin.lines:
if (getattr(line, 'invoice', None)
and line.invoice.currency == self.company.currency):
invoices.add(line.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 (getattr(line, 'invoice', None) 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', 'journal', 'company')
def on_change_lines(self):
pool = Pool()
Line = pool.get('account.statement.line')
if not self.journal or not self.lines or not self.company:
return
if self.journal.currency != self.company.currency:
return
invoices = set()
for line in self.lines:
if (getattr(line, 'invoice', None)
and line.invoice.currency == self.company.currency):
invoices.add(line.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 getattr(line, 'invoice', None) and line.id:
if line.invoice.id not in invoice_id2amount_to_pay:
continue
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):
new_line = Line()
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
new_line.invoice = None
line_offset += 1
lines.insert(index + line_offset, new_line)
invoice_id2amount_to_pay[line.invoice.id] = 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
@fields.depends('journal')
def on_change_with_validation(self, name=None):
if self.journal:
return self.journal.validation
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
def _group_key(self, line):
key = (
('number', line.number or Unequal()),
('date', line.date),
('party', line.party),
)
return key
def _get_grouped_line(self):
"Return Line class for grouped lines"
lines = self.origins or self.lines
assert lines
keys = [k[0] for k in self._group_key(lines[0])]
class Line(namedtuple('Line', keys + ['lines'])):
@property
def amount(self):
return sum((l.amount for l in self.lines))
@property
def descriptions(self):
done = set()
for line in self.lines:
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:
lines = self.origins
elif self.lines:
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)),)))
@classmethod
def delete(cls, statements):
# Cancel before delete
cls.cancel(statements)
for statement in statements:
if statement.state != 'cancel':
raise AccessError(
gettext('account_statement.msg_statement_delete_cancel',
statement=statement.rec_name))
super(Statement, cls).delete(statements)
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, statements):
pass
def validate_balance(self):
pool = Pool()
Lang = pool.get('ir.lang')
amount = (self.start_balance
+ sum(l.amount for l in self.lines))
if amount != self.end_balance:
lang = Lang.get()
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))
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:
lang = Lang.get()
total_amount = lang.currency(
self.total_amount, self.journal.currency)
amount = lang.currency(amount, self.journal.currency)
raise StatementValidateError(
gettext('account_statement.msg_statement_wrong_total_amount',
statement=self.rec_name,
total_amount=total_amount,
amount=amount))
def validate_number_of_lines(self):
number = len(list(self.grouped_lines))
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))
@classmethod
@ModelView.button
@Workflow.transition('validated')
def validate_statement(cls, statements):
pool = Pool()
Line = pool.get('account.statement.line')
Warning = pool.get('res.user.warning')
for statement in statements:
getattr(statement, 'validate_%s' % statement.validation)()
cls.create_move(statements)
cls.write(statements, {
'state': 'validated',
})
common_lines = Line.search([
('statement.state', '=', 'draft'),
('invoice.state', '=', 'paid'),
])
if common_lines:
warning_key = '_'.join(str(l.id) for l in common_lines)
if Warning.check(warning_key):
raise StatementValidateWarning(warning_key,
gettext('account_statement'
'.msg_statement_paid_invoice_draft'))
Line.write(common_lines, {
'invoice': None,
})
@classmethod
def create_move(cls, statements):
'''Create move for the statements and try to reconcile the lines.
Returns the list of move, statement and lines
'''
pool = Pool()
Line = pool.get('account.statement.line')
Move = pool.get('account.move')
MoveLine = pool.get('account.move.line')
moves = []
for statement in statements:
for key, lines in groupby(
statement.lines, key=statement._group_key):
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)
move_lines = []
for move, statement, lines in moves:
amount = 0
amount_second_currency = 0
for line in lines:
move_line = line.get_move_line()
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(
amount, amount_second_currency, lines)
move_line.move = move
move_lines.append((move_line, None))
MoveLine.save([l for l, _ in move_lines])
Line.reconcile(move_lines)
return moves
def _get_move(self, key):
'Return Move for the grouping key'
pool = Pool()
Move = pool.get('account.move')
Period = pool.get('account.period')
period_id = Period.find(self.company.id, date=key['date'])
return Move(
period=period_id,
journal=self.journal.journal,
date=key['date'],
origin=self,
company=self.company,
description=str(key['number']),
)
def _get_move_line(self, amount, amount_second_currency, lines):
'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
else:
second_currency = None
amount_second_currency = None
descriptions = {l.description for l in lines}
if len(descriptions) == 1:
description, = descriptions
else:
description = ''
return MoveLine(
debit=abs(amount) if amount < 0 else 0,
credit=abs(amount) if amount > 0 else 0,
account=self.journal.account,
second_currency=second_currency,
amount_second_currency=amount_second_currency,
description=description,
)
@classmethod
@ModelView.button
@Workflow.transition('posted')
def post(cls, statements):
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:
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))
lines = [l for s in statements for l in s.lines]
StatementLine.post_move(lines)
@classmethod
@ModelView.button
@Workflow.transition('cancel')
def cancel(cls, statements):
StatementLine = Pool().get('account.statement.line')
lines = [l for s in statements for l in s.lines]
StatementLine.delete_move(lines)
@classmethod
@ModelView.button_action('account_statement.act_reconcile')
def reconcile(cls, statements):
pass
def origin_mixin(_states, _depends):
class Mixin:
statement = fields.Many2One(
'account.statement', "Statement",
required=True, ondelete='CASCADE', states=_states,
depends=_depends)
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')
number = fields.Char("Number")
date = fields.Date(
"Date", required=True, states=_states, depends=_depends)
amount = fields.Numeric(
"Amount", required=True,
digits=(16, Eval('_parent_statement', {})
.get('currency_digits', 2)),
states=_states, depends=_depends)
party = fields.Many2One(
'party.party', "Party", states=_states, depends=_depends)
account = fields.Many2One(
'account.account', "Account",
domain=[
('company', '=', Eval('company', 0)),
('type', '!=', None),
('closed', '!=', True),
],
context={
'date': Eval('date'),
},
states=_states, depends=_depends + ['company', 'date'])
description = fields.Char(
"Description", states=_states, depends=_depends)
@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):
if self.statement and self.statement.company:
return self.statement.company.id
@classmethod
def search_company(cls, name, clause):
return [('statement.' + clause[0],) + tuple(clause[1:])]
return Mixin
_states = {
'readonly': Eval('statement_state') != 'draft',
}
_depends = ['statement_state']
class Line(
origin_mixin(_states, _depends), sequence_ordered(),
ModelSQL, ModelView):
'Account Statement Line'
__name__ = 'account.statement.line'
move = fields.Many2One('account.move', 'Account Move', readonly=True,
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
invoice = fields.Many2One('account.invoice', 'Invoice',
domain=[
If(Bool(Eval('party')), [('party', '=', Eval('party'))], []),
If(Bool(Eval('account')), [('account', '=', Eval('account'))], []),
If(Eval('statement_state') == 'draft',
('state', '=', 'posted'),
('state', '!=', '')),
],
states=_states,
context={'with_payment': False},
depends=['party', 'account'] + _depends)
origin = fields.Many2One('account.statement.origin', 'Origin',
readonly=True,
states={
'invisible': ~Bool(Eval('origin')),
},
domain=[
('statement', '=', Eval('statement')),
('date', '=', Eval('date')),
],
depends=['statement', 'date'])
@classmethod
def __setup__(cls):
super(Line, cls).__setup__()
if 'origin' not in cls.date.depends:
cls.date.states.update({
'readonly': (cls.date.states['readonly'] |
Bool(Eval('origin', 0))),
})
cls.date.depends.append('origin')
cls.account.required = True
t = cls.__table__()
cls._sql_constraints += [
('check_statement_line_amount', Check(t, t.amount != 0),
'account_statement.msg_line_amount_non_zero'),
]
@staticmethod
def default_amount():
return Decimal(0)
@fields.depends('amount', 'party', 'invoice', 'date')
def on_change_party(self):
if self.party:
if self.amount:
with Transaction().set_context(date=self.date):
if self.amount > Decimal("0.0"):
self.account = self.party.account_receivable_used
else:
self.account = self.party.account_payable_used
if self.invoice:
if self.party:
if self.invoice.party != self.party:
self.invoice = None
else:
self.invoice = None
@fields.depends('amount', 'party', 'account', 'invoice', 'statement',
'date', '_parent_statement.journal')
def on_change_amount(self):
Currency = Pool().get('currency.currency')
if self.party:
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.0"):
self.account = self.party.account_receivable_used
else:
self.account = self.party.account_payable_used
if self.invoice:
if self.amount and self.statement and self.statement.journal:
invoice = self.invoice
journal = self.statement.journal
with Transaction().set_context(date=invoice.currency_date):
amount_to_pay = Currency.compute(invoice.currency,
invoice.amount_to_pay, journal.currency)
if abs(self.amount) > amount_to_pay:
self.invoice = None
else:
self.invoice = None
@fields.depends('account', 'invoice')
def on_change_account(self):
if self.invoice:
if self.account:
if self.invoice.account != self.account:
self.invoice = None
else:
self.invoice = None
@fields.depends('party', 'account', 'invoice')
def on_change_invoice(self):
if self.invoice:
if not self.party:
self.party = self.invoice.party
if not self.account:
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',
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:
company = super(Line, self).on_change_with_company()
except AttributeError:
company = None
if self.origin and self.origin.company:
return self.origin.company.id
return company
@fields.depends('origin', '_parent_origin.state')
def on_change_with_statement_state(self, name=None):
try:
state = super(Line, self).on_change_with_statement_state()
except AttributeError:
state = None
if self.origin:
return self.origin.statement_state
return state
def get_rec_name(self, name):
return self.statement.rec_name
@classmethod
def search_rec_name(cls, name, clause):
return [('statement.rec_name',) + tuple(clause[1:])]
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('move', None)
default.setdefault('invoice', None)
return super(Line, cls).copy(lines, default=default)
@classmethod
def reconcile(cls, move_lines):
pool = Pool()
Invoice = pool.get('account.invoice')
MoveLine = pool.get('account.move.line')
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:
Invoice.add_payment_lines(invoice_payments)
invoice_payments.clear()
reconcile_lines = line.invoice.get_reconcile_lines_for_amount(
move_line.credit - move_line.debit)
assert move_line.account == line.invoice.account
invoice_payments[line.invoice].append(move_line.id)
if not reconcile_lines[1]:
to_reconcile.append(reconcile_lines[0] + [move_line])
if invoice_payments:
Invoice.add_payment_lines(invoice_payments)
if to_reconcile:
MoveLine.reconcile(*to_reconcile)
@classmethod
def post_move(cls, lines):
Move = Pool().get('account.move')
Move.post(list({l.move for l in lines if l.move}))
@classmethod
def delete_move(cls, lines):
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]
Reconciliation.delete(reconciliations)
Move.delete(list({l.move for l in lines if l.move}))
def get_move_line(self):
'''
Return the move line for the statement line
'''
pool = Pool()
MoveLine = pool.get('account.move.line')
Currency = Pool().get('currency.currency')
zero = Decimal("0.0")
with Transaction().set_context(date=self.date):
amount = Currency.compute(self.statement.journal.currency,
self.amount, self.statement.company.currency)
if self.statement.journal.currency != self.statement.company.currency:
second_currency = self.statement.journal.currency.id
amount_second_currency = -self.amount
else:
amount_second_currency = None
second_currency = None
return MoveLine(
description=self.description,
debit=amount < zero and -amount or zero,
credit=amount >= zero and amount or zero,
account=self.account,
party=self.party if self.account.party_required else None,
second_currency=second_currency,
amount_second_currency=amount_second_currency,
)
del _states, _depends
class LineGroup(ModelSQL, ModelView):
'Account Statement Line Group'
__name__ = 'account.statement.line.group'
_rec_name = 'number'
statement = fields.Many2One('account.statement', 'Statement')
journal = fields.Function(fields.Many2One('account.statement.journal',
'Journal'), 'get_journal', searcher='search_journal')
number = fields.Char('Number')
date = fields.Date('Date')
amount = fields.Numeric('Amount',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
currency = fields.Function(fields.Many2One('currency.currency',
'Currency'), 'get_currency')
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'get_currency_digits')
party = fields.Many2One('party.party', 'Party')
move = fields.Many2One('account.move', 'Move')
@classmethod
def __setup__(cls):
super(LineGroup, cls).__setup__()
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'),
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,
move.create_uid,
move.create_date,
move.write_uid,
move.write_date,
]
columns = (std_columns + [move.id.as_('move')]
+ cls._grouped_columns(line))
return move.join(line,
condition=move.id == line.move
).select(*columns,
where=move.origin.ilike(Statement.__name__ + ',%'),
group_by=std_columns + [move.id]
)
def get_journal(self, name):
return self.statement.journal.id
@classmethod
def search_journal(cls, name, clause):
return [('statement.' + clause[0],) + tuple(clause[1:])]
def get_currency(self, name):
return self.statement.journal.currency.id
def get_currency_digits(self, name):
return self.statement.journal.currency.digits
_states = {
'readonly': (Eval('statement_state') != 'draft') | Eval('lines', []),
}
_depends = ['statement_state']
class Origin(origin_mixin(_states, _depends), ModelSQL, ModelView):
"Account Statement Origin"
__name__ = 'account.statement.origin'
_rec_name = 'number'
lines = fields.One2Many(
'account.statement.line', 'origin', "Lines",
states={
'readonly': ((Eval('statement_id', -1) < 0) |
~Eval('statement_state').in_(['draft', 'validated'])),
},
domain=[
('statement', '=', Eval('statement')),
('date', '=', Eval('date')),
],
depends=['statement', 'date', 'statement_id'])
statement_id = fields.Function(
fields.Integer("Statement ID"), 'on_change_with_statement_id')
pending_amount = fields.Function(
fields.Numeric("Pending Amount",
digits=(16, Eval('_parent_statement', {})
.get('currency_digits', 2))),
'on_change_with_pending_amount', searcher='search_pending_amount')
information = fields.Dict(
'account.statement.origin.information', "Information", readonly=True)
@classmethod
def __register__(cls, module_name):
table = cls.__table_handler__(module_name)
# Migration from 5.0: rename informations into information
table.column_rename('informations', 'information')
super(Origin, cls).__register__(module_name)
@fields.depends('statement')
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)]
del _states, _depends
class OriginInformation(DictSchemaMixin, ModelSQL, ModelView):
"Statement Origin Information"
__name__ = 'account.statement.origin.information'
class ImportStatementStart(ModelView):
"Statement Import Start"
__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)
@classmethod
def default_file_format(cls):
return None
@classmethod
def default_company(cls):
return Transaction().context.get('company')
class ImportStatement(Wizard):
"Statement Import"
__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)
data = {'res_id': list(map(int, statements))}
if len(statements) == 1:
action['views'].reverse()
return action, data
class ReconcileStatement(Wizard):
"Statement Reconcile"
__name__ = 'account.statement.reconcile'
start = StateAction('account.act_reconcile')
def do_start(self, action):
pool = Pool()
Statement = pool.get('account.statement')
statements = Statement.browse(Transaction().context['active_ids'])
lines = sum(([int(l) for l in s.lines_to_reconcile]
for s in statements), [])
return action, {
'model': 'account.move.line',
'ids': lines,
}
class StatementReport(CompanyReport):
__name__ = 'account.statement'