trytond-account_bank/account.py

737 lines
28 KiB
Python

# This file is part of account_bank module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from sql import Null
from sql.aggregate import BoolOr
from sql.operators import In
from decimal import Decimal
from trytond.model import ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Bool, If
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateTransition, StateView, Button
from trytond.i18n import gettext
from trytond.exceptions import UserError
__all__ = ['PaymentType', 'BankAccount', 'Party', 'Invoice', 'Reconciliation',
'Line', 'CompensationMoveStart', 'CompensationMove']
ACCOUNT_BANK_KIND = [
('none', 'None'),
('party', 'Party'),
('company', 'Company'),
('other', 'Other'),
]
class PaymentType(metaclass=PoolMeta):
__name__ = 'account.payment.type'
account_bank = fields.Selection(ACCOUNT_BANK_KIND, 'Account Bank Kind',
required=True)
party = fields.Many2One('party.party', 'Party',
states={
'required': Eval('account_bank') == 'other',
'invisible': Eval('account_bank') != 'other',
},
context={
'company': Eval('company', -1),
},
depends=['account_bank', 'company'])
bank_account = fields.Many2One('bank.account', 'Bank Account',
domain=[
If(Eval('party', None) == None,
('id', '=', -1),
('owners.id', '=', Eval('party')),
),
],
states={
'required': Eval('account_bank') == 'other',
'invisible': Eval('account_bank') != 'other',
},
depends=['party', 'account_bank'])
@classmethod
def __setup__(cls):
super(PaymentType, cls).__setup__()
cls._check_modify_fields |= set(['account_bank', 'party',
'bank_account'])
@staticmethod
def default_account_bank():
return 'none'
class BankAccount(metaclass=PoolMeta):
__name__ = 'bank.account'
@classmethod
def __setup__(cls):
super(BankAccount, cls).__setup__()
cls._check_owners_fields = set(['owners'])
cls._check_owners_related_models = set([
('account.move.line', 'bank_account'),
('account.invoice', 'bank_account'),
])
@classmethod
def write(cls, *args):
actions = iter(args)
all_accounts = []
for accounts, values in zip(actions, actions):
if set(values.keys()) & cls._check_owners_fields:
all_accounts += accounts
super(BankAccount, cls).write(*args)
cls.check_owners(all_accounts)
@classmethod
def check_owners(cls, accounts):
with Transaction().set_context(_check_access=False):
pool = Pool()
IrModel = pool.get('ir.model')
Field = pool.get('ir.model.field')
account_ids = [a.id for a in accounts]
for value in cls._check_owners_related_models:
model_name, field_name = value
Model = pool.get(model_name)
records = Model.search([(field_name, 'in', account_ids)])
model, = IrModel.search([('model', '=', model_name)])
field, = Field.search([
('model.model', '=', model_name),
('name', '=', field_name),
], limit=1)
for record in records:
target = record.account_bank_from
account = getattr(record, field_name)
if target not in account.owners:
raise UserError(gettext(
'account_bank.modify_with_related_model',
account=account.rec_name,
model=model.name,
field=field.field_description,
name=record.rec_name))
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
@classmethod
def write(cls, *args):
pool = Pool()
BankAccount = pool.get('bank.account')
actions = iter(args)
all_accounts = []
for parties, values in zip(actions, actions):
if set(values.keys()) & set(['bank_accounts']):
all_accounts += list(set(
[a for p in parties for a in p.bank_accounts]))
super(Party, cls).write(*args)
BankAccount.check_owners(all_accounts)
class BankMixin(object):
__slots__ = ()
account_bank = fields.Function(fields.Selection(ACCOUNT_BANK_KIND,
'Account Bank'),
'on_change_with_account_bank')
account_bank_from = fields.Function(fields.Many2One('party.party',
'Account Bank From'),
'on_change_with_account_bank_from')
bank_account = fields.Many2One('bank.account', 'Bank Account',
domain=[
If(Eval('account_bank_from', None) == None,
('id', '=', -1),
('owners.id', '=', Eval('account_bank_from')),
),
],
states={
'readonly': Eval('account_bank') == 'other',
'invisible': ~Bool(Eval('account_bank_from')),
},
depends=['party', 'payment_type', 'account_bank_from', 'account_bank'],
ondelete='RESTRICT')
@fields.depends('payment_type')
def on_change_with_account_bank(self, name=None):
if self.payment_type:
return self.payment_type.account_bank
def _get_bank_account(self):
pool = Pool()
Party = pool.get('party.party')
if self.party and self.payment_type:
if self.payment_type.account_bank == 'other':
self.bank_account = self.payment_type.bank_account
else:
party_fname = '%s_bank_account' % self.payment_type.kind
if hasattr(Party, party_fname):
account_bank = self.payment_type.account_bank
if account_bank == 'company':
if hasattr(self, 'company') and self.company:
available_banks = getattr(self.company.party,
'bank_accounts', [])
if self.bank_account in available_banks:
return
party_company_fname = ('%s_company_bank_account' %
self.payment_type.kind)
company_bank = getattr(self.party, party_company_fname,
None)
if company_bank:
self.bank_account = company_bank
elif hasattr(self, 'company') and self.company:
default_bank = getattr(
self.company.party, party_fname)
self.bank_account = default_bank
return
elif account_bank == 'party' and self.party:
default_bank = getattr(self.party, party_fname)
if (hasattr(self, 'bank_account') and self.bank_account
and self.bank_account == default_bank):
return
self.bank_account = default_bank
return
else:
self.bank_account = None
return
else:
self.bank_account = None
return
@fields.depends('party', 'payment_type', 'bank_account',
methods=['on_change_with_payment_type'])
def on_change_payment_type(self):
self._get_bank_account()
@fields.depends('payment_type', 'party',
methods=['on_change_with_payment_type'])
def on_change_with_account_bank_from(self, name=None):
'''
Sets the party where get bank account for this move line.
'''
Company = Pool().get('company.company')
if self.payment_type and self.party:
payment_type = self.payment_type
party = self.party
if payment_type.account_bank == 'party':
return party.id
elif payment_type.account_bank == 'company':
company = Transaction().context.get('company')
if company:
return Company(company).party.id
elif payment_type.account_bank == 'other':
return payment_type.party.id
class Invoice(BankMixin, metaclass=PoolMeta):
__name__ = 'account.invoice'
@classmethod
def __setup__(cls):
super(Invoice, cls).__setup__()
readonly = ~Eval('state').in_(['draft', 'validated'])
previous_readonly = cls.bank_account.states.get('readonly')
if previous_readonly:
readonly = readonly | previous_readonly
cls.bank_account.states.update({
'readonly': readonly,
})
cls.account_bank_from.context = {'company': Eval('company', -1)}
cls.account_bank_from.depends = ['company']
# allow process or paid invoices when is posted
cls._check_modify_exclude.add('bank_account')
@fields.depends('payment_type', 'party', 'company', 'bank_account')
def on_change_party(self):
'''
Add account bank to invoice line when changes party.
'''
super(Invoice, self).on_change_party()
self.bank_account = None
if self.payment_type:
self._get_bank_account()
@classmethod
def post(cls, invoices):
'''
Check up invoices that requires bank account because its payment type,
has one
'''
to_save = []
for invoice in invoices:
account_bank = (invoice.payment_type and
invoice.payment_type.account_bank or 'none')
if (invoice.payment_type and account_bank != 'none'
and not invoice.bank_account):
invoice._get_bank_account()
if not invoice.bank_account:
raise UserError(gettext(
'account_bank.invoice_without_bank_account',
invoice=invoice.rec_name,
payment_type=invoice.payment_type.rec_name))
to_save.append(invoice)
if to_save:
cls.save(to_save)
super(Invoice, cls).post(invoices)
def _get_move_line(self, date, amount):
'''Add account bank to move line when post invoice.'''
line = super(Invoice, self)._get_move_line(date, amount)
if self.bank_account:
line.bank_account = self.bank_account
return line
@fields.depends('payment_type', 'party', 'company', 'bank_account')
def on_change_lines(self):
super().on_change_lines()
self.bank_account = None
if self.payment_type:
self._get_bank_account()
class Reconciliation(metaclass=PoolMeta):
__name__ = 'account.move.reconciliation'
@classmethod
def create(cls, vlist):
Invoice = Pool().get('account.invoice')
reconciliations = super(Reconciliation, cls).create(vlist)
moves = set()
for reconciliation in reconciliations:
moves |= set(l.move for l in reconciliation.lines)
invoices = set()
for move in moves:
if (move.origin and isinstance(move.origin, Invoice)
and move.origin.state == 'posted'):
invoices.add(move.origin)
if invoices:
Invoice.process(invoices)
return reconciliations
@classmethod
def delete(cls, reconciliations):
Invoice = Pool().get('account.invoice')
moves = set()
for reconciliation in reconciliations:
moves |= set(l.move for l in reconciliation.lines)
invoices = set()
for move in moves:
if move.origin and isinstance(move.origin, Invoice):
invoices.add(move.origin)
super(Reconciliation, cls).delete(reconciliations)
if invoices:
Invoice.process(invoices)
class Line(BankMixin, metaclass=PoolMeta):
__name__ = 'account.move.line'
reverse_moves = fields.Function(fields.Boolean('With Reverse Moves'),
'get_reverse_moves', searcher='search_reverse_moves')
netting_moves = fields.Function(fields.Boolean('With Netting Moves'),
'get_netting_moves', searcher='search_netting_moves')
@classmethod
def __setup__(cls):
super(Line, cls).__setup__()
if hasattr(cls, '_check_modify_exclude'):
cls._check_modify_exclude.add('bank_account')
readonly = Bool(Eval('reconciliation'))
previous_readonly = cls.bank_account.states.get('readonly')
if previous_readonly:
readonly = readonly | previous_readonly
cls.bank_account.states.update({
'readonly': readonly,
})
cls.account_bank_from.context = {'company': Eval('company', -1)}
cls.account_bank_from.depends.add('company')
@fields.depends('party', 'payment_type', 'bank_account')
def on_change_party(self):
'''Add account bank to account move line when changes party.'''
try:
super(Line, self).on_change_party()
except AttributeError:
pass
if self.payment_type and self.party:
self._get_bank_account()
@fields.depends('party', 'account_kind', 'move', '_parent_move.id')
def on_change_with_payment_type(self, name=None):
if self.party:
if self.account_kind == 'payable':
return (self.party.supplier_payment_type.id
if self.party.supplier_payment_type else None)
elif self.account_kind == 'receivable':
return (self.party.customer_payment_type.id
if self.party.customer_payment_type else None)
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
if (Transaction().context.get('cancel_move')
and 'bank_account' not in default):
default['bank_account'] = None
return super(Line, cls).copy(lines, default)
def get_reverse_moves(self, name):
if (not self.account
or (self.account.type.receivable == False and
self.account.type.payable == False)):
return False
domain = [
('account', '=', self.account.id),
('reconciliation', '=', None),
]
if self.party:
domain.append(('party', '=', self.party.id))
if self.credit > Decimal('0.0'):
domain.append(('debit', '>', 0))
if self.debit > Decimal('0.0'):
domain.append(('credit', '>', 0))
moves = self.search(domain, limit=1)
return len(moves) > 0
@classmethod
def search_reverse_moves(cls, name, clause):
pool = Pool()
Account = pool.get('account.account')
MoveLine = pool.get('account.move.line')
operator = 'in' if clause[2] else 'not in'
lines = MoveLine.__table__()
move_line = MoveLine.__table__()
account = Account.__table__()
cursor = Transaction().connection.cursor()
reverse = move_line.join(account, condition=(
account.id == move_line.account)).select(
move_line.account, move_line.party,
where=(account.reconcile
& (move_line.reconciliation == Null)),
group_by=(move_line.account, move_line.party),
having=((BoolOr((move_line.debit) != Decimal(0)))
& (BoolOr((move_line.credit) != Decimal(0))))
)
query = lines.select(lines.id, where=(
In((lines.account, lines.party), reverse)))
# Fetch the data otherwise its too slow
cursor.execute(*query)
return [('id', operator, [x[0] for x in cursor.fetchall()])]
def get_netting_moves(self, name):
if (not self.account
or (self.account.type.receivable == False and
self.account.type.payable == False)):
return False
if not self.account.party_required:
return False
domain = [
('party', '=', self.party.id),
('reconciliation', '=', None),
['OR',
('debit', '!=', 0),
('credit', '!=', 0),
],
['OR',
('account.type.receivable', '=', True),
('account.type.payable', '=', True)
],
('move.company', '=', self.move.company)
]
moves = self.search(domain, limit=1)
return len(moves) > 0
@classmethod
def search_netting_moves(cls, name, clause):
pool = Pool()
Account = pool.get('account.account')
MoveLine = pool.get('account.move.line')
Move = pool.get('account.move')
AccountType = pool.get('account.account.type')
Rule = pool.get('ir.rule')
operator = 'in' if clause[2] else 'not in'
lines = MoveLine.__table__()
move = Move.__table__()
move_line = MoveLine.__table__()
account = Account.__table__()
account_type = AccountType.__table__()
cursor = Transaction().connection.cursor()
companies = Rule._get_context(cls.__name__).get('companies')
if not companies:
companies = [-1]
company_filter = move.company.in_(companies)
netting = move_line.join(account, condition=(
account.id == move_line.account)).join(move, condition=(
move.id == move_line.move)).join(account_type, condition=(
account_type.id == account.type)).select(
move.company, move_line.party,
where=(account.reconcile
& (move_line.reconciliation == Null)
& (move.state == 'posted')
& (account_type.receivable | account_type.payable)
& (move_line.party != Null))
& company_filter,
group_by=(move_line.party, move.company),
having=((BoolOr((move_line.debit) != Decimal(0)))
& (BoolOr((move_line.credit) != Decimal(0))))
)
query = lines.join(move, condition=(move.id == lines.move)).select(lines.id, where=(
In((move.company, lines.party), netting)))
# Fetch the data otherwise its too slow
cursor.execute(*query)
return [('id', operator, [x[0] for x in cursor.fetchall()])]
@fields.depends('_parent_move.id')
def on_change_with_account_bank_from(self, name=None):
return super().on_change_with_account_bank_from(name)
def get_payment_kind(self, name):
# From https://discuss.tryton.org/t/field-amount-to-pay-in-account-payment/6561/7
kind = super().get_payment_kind(name)
if not kind:
if self.account.type.receivable:
kind = 'receivable'
elif self.account.type.payable:
kind = 'payable'
return kind
class CompensationMoveStart(ModelView, BankMixin):
'Create Compensation Move Start'
__name__ = 'account.move.compensation_move.start'
party = fields.Many2One('party.party', 'Party', readonly=True)
account = fields.Many2One('account.account', 'Account',
domain=[
('company', '=', Eval('context', {}).get('company', -1)),
['OR',
('type.receivable', '=', True),
('type.payable','=', True)]],
required=True)
date = fields.Date('Date')
maturity_date = fields.Date('Maturity Date')
description = fields.Char('Description')
payment_kind = fields.Selection([
('both', 'Both'),
('payable', 'Payable'),
('receivable', 'Receivable'),
], 'Payment Kind')
payment_type = fields.Many2One('account.payment.type', 'Payment Type',
domain=[
('kind', '=', Eval('payment_kind'))
],
depends=['payment_kind'])
@staticmethod
def default_date():
pool = Pool()
return pool.get('ir.date').today()
@staticmethod
def default_maturity_date():
pool = Pool()
return pool.get('ir.date').today()
@classmethod
def default_get(cls, fields, with_rec_name=True):
pool = Pool()
Line = pool.get('account.move.line')
PaymentType = pool.get('account.payment.type')
defaults = super(CompensationMoveStart, cls).default_get(fields,
with_rec_name)
party = None
company = None
amount = Decimal('0.0')
lines = Line.browse(Transaction().context.get('active_ids', []))
for line in lines:
amount += line.debit - line.credit
if not party:
party = line.party
elif party != line.party:
raise UserError(gettext('account_bank.different_parties',
party=line.party.rec_name, line=line.rec_name,
previous_party=party.rec_name))
if not company:
company = line.account.company
if (company and company.currency.is_zero(amount)
and len(set([x.account for x in lines])) == 1):
raise UserError(gettext('account_bank.normal_reconcile'))
if amount > 0:
defaults['payment_kind'] = 'receivable'
else:
defaults['payment_kind'] = 'payable'
defaults['bank_account'] = None
if party:
defaults['party'] = party.id
if (defaults['payment_kind'] in ['receivable', 'both']
and party.customer_payment_type):
defaults['payment_type'] = party.customer_payment_type.id
elif (defaults['payment_kind'] in ['payable', 'both']
and party.supplier_payment_type):
defaults['payment_type'] = party.supplier_payment_type.id
if defaults.get('payment_type'):
payment_type = PaymentType(defaults['payment_type'])
defaults['account_bank'] = payment_type.account_bank
self = cls()
self.payment_type = payment_type
self.party = party
self._get_bank_account()
defaults['account_bank_from'] = (
self.on_change_with_account_bank_from())
defaults['bank_account'] = (self.bank_account.id
if hasattr(self, 'bank_account') and self.bank_account else None)
if amount > 0:
defaults['account'] = (party.account_receivable.id
if party.account_receivable else None)
else:
defaults['account'] = (party.account_payable.id
if party.account_payable else None)
return defaults
def on_change_with_payment_type(self, name=None):
pass
class CompensationMove(Wizard):
'Create Compensation Move'
__name__ = 'account.move.compensation_move'
start = StateView('account.move.compensation_move.start',
'account_bank.compensation_move_lines_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'create_move', 'tryton-ok', default=True),
])
create_move = StateTransition()
def transition_create_move(self):
pool = Pool()
Move = pool.get('account.move')
Line = pool.get('account.move.line')
move_lines = []
lines = Line.browse(Transaction().context.get('active_ids'))
for line in lines:
if ((line.account.type.receivable == False and
line.account.type.payable == False)
or line.reconciliation):
continue
move_lines.append(self.get_counterpart_line(line))
if not lines or not move_lines:
return 'end'
move = self.get_move(lines)
extra_lines, origin = self.get_extra_lines(lines, self.start.account,
self.start.party)
if origin:
move.origin = origin
move.lines = move_lines + extra_lines
move.save()
Move.post([move])
to_reconcile = {}
for line in lines:
to_reconcile.setdefault(line.account.id, []).append(line)
for line in move.lines:
append = True
for extra_line in extra_lines:
if self.is_extra_line(line, extra_line):
append = False
break
if append:
to_reconcile.setdefault(line.account.id, []).append(line)
for lines_to_reconcile in to_reconcile.values():
Line.reconcile(lines_to_reconcile)
return 'end'
def is_extra_line(self, line, extra_line):
" Returns true if both lines are equal"
return (line.debit == extra_line.debit and
line.credit == extra_line.credit and
line.maturity_date == extra_line.maturity_date and
line.payment_type == extra_line.payment_type and
line.bank_account == extra_line.bank_account)
def get_counterpart_line(self, line):
'Returns the counterpart line to create from line'
pool = Pool()
Line = pool.get('account.move.line')
new_line = Line()
new_line.account = line.account
new_line.debit = line.credit
new_line.credit = line.debit
new_line.description = line.description
new_line.second_currency = line.second_currency
if line.second_currency:
new_line.amount_second_currency = -line.amount_second_currency
new_line.party = line.party
return new_line
def get_move(self, lines):
'Returns the new move to create from lines'
pool = Pool()
Move = pool.get('account.move')
Period = pool.get('account.period')
Date = pool.get('ir.date')
period_id = Period.find(lines[0].account.company.id)
move = Move()
move.period = Period(period_id)
move.journal = lines[0].move.journal
move.date = Date.today()
move.description = self.start.description
return move
def get_extra_lines(self, lines, account, party):
'Returns extra lines to balance move and move origin'
pool = Pool()
Line = pool.get('account.move.line')
amount = Decimal('0.0')
origins = {}
for line in lines:
line_amount = line.debit - line.credit
amount += line_amount
if line.origin:
if line.origin not in origins:
origins[line.origin] = Decimal('0.0')
origins[line.origin] += abs(line_amount)
if not account or not party:
return ([], None)
extra_line = Line()
extra_line.account = account
extra_line.party = party
extra_line.maturity_date = self.start.maturity_date
extra_line.payment_type = self.start.payment_type
extra_line.bank_account = self.start.bank_account
extra_line.description = self.start.description
extra_line.credit = extra_line.debit = Decimal('0.0')
if amount > 0:
extra_line.debit = amount
else:
extra_line.credit = abs(amount)
origin = None
for line_origin, line_amount in sorted(origins.items(),
key=lambda x: x[1]):
if abs(amount) < line_amount:
origin = line_origin
break
return [extra_line], origin