1062 lines
39 KiB
Python
1062 lines
39 KiB
Python
# This file is part of the account_voucher_ar module for 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 defaultdict, namedtuple
|
|
from itertools import combinations
|
|
|
|
from trytond.model import Workflow, ModelView, ModelSQL, fields, Index
|
|
from trytond.report import Report
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, In, If, Bool
|
|
from trytond.tools import grouped_slice
|
|
from trytond.transaction import Transaction
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
|
|
_ZERO = Decimal('0.0')
|
|
|
|
|
|
class AccountVoucherPayMode(ModelSQL, ModelView):
|
|
'Account Voucher Pay Mode'
|
|
__name__ = 'account.voucher.paymode'
|
|
|
|
name = fields.Char('Name')
|
|
account = fields.Many2One('account.account', 'Account',
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
])
|
|
|
|
|
|
class AccountVoucher(Workflow, ModelSQL, ModelView):
|
|
'Account Voucher'
|
|
__name__ = 'account.voucher'
|
|
_rec_name = 'number'
|
|
|
|
_states = {'readonly': Eval('state') != 'draft'}
|
|
_states_done = {'readonly': In(Eval('state'), ['posted', 'cancelled'])}
|
|
|
|
number = fields.Char('Number', readonly=True, help="Voucher Number")
|
|
party = fields.Many2One('party.party', 'Party', required=True,
|
|
context={'company': Eval('company', -1)},
|
|
states=_states, depends={'company'})
|
|
voucher_type = fields.Selection([
|
|
('payment', 'Payment'),
|
|
('receipt', 'Receipt'),
|
|
], 'Type', required=True, states=_states)
|
|
pay_lines = fields.One2Many('account.voucher.line.paymode', 'voucher',
|
|
'Pay Mode Lines', states=_states_done)
|
|
date = fields.Date('Date', required=True, states=_states)
|
|
journal = fields.Many2One('account.journal', 'Journal', required=True,
|
|
context={'company': Eval('company', -1)},
|
|
states=_states, depends={'company'})
|
|
currency = fields.Many2One('currency.currency', 'Currency', required=True,
|
|
states=_states)
|
|
currency_rate = fields.Numeric('Currency rate', digits=(12, 6),
|
|
states=_states_done)
|
|
currency_code = fields.Function(fields.Char('Currency Code'),
|
|
'on_change_with_currency_code')
|
|
company = fields.Many2One('company.company', 'Company', states=_states)
|
|
lines = fields.One2Many('account.voucher.line', 'voucher', 'Lines',
|
|
states=_states)
|
|
lines_credits = fields.One2Many('account.voucher.line.credits', 'voucher',
|
|
'Credits', states={
|
|
'invisible': ~Eval('lines_credits'),
|
|
'readonly': In(Eval('state'), ['posted', 'cancelled']),
|
|
})
|
|
lines_debits = fields.One2Many('account.voucher.line.debits', 'voucher',
|
|
'Debits', states={
|
|
'invisible': ~Eval('lines_debits'),
|
|
'readonly': In(Eval('state'), ['posted', 'cancelled']),
|
|
})
|
|
comment = fields.Text('Comment', states=_states_done)
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('posted', 'Posted'),
|
|
('cancelled', 'Cancelled'),
|
|
], 'State', readonly=True)
|
|
amount = fields.Function(fields.Numeric('Payment', digits=(16, 2)),
|
|
'on_change_with_amount')
|
|
amount_to_pay = fields.Function(fields.Numeric('To Pay', digits=(16, 2)),
|
|
'on_change_with_amount_to_pay')
|
|
amount_invoices = fields.Function(fields.Numeric('Invoices',
|
|
digits=(16, 2)), 'on_change_with_amount_invoices')
|
|
move = fields.Many2One('account.move', 'Move', readonly=True)
|
|
move_cancelled = fields.Many2One('account.move', 'Move Cancelled',
|
|
readonly=True, states={'invisible': ~Eval('move_cancelled')})
|
|
pay_invoice = fields.Many2One('account.invoice', 'Pay Invoice')
|
|
writeoff = fields.Many2One('account.move.reconcile.write_off',
|
|
'Write Off', domain=[('company', '=', Eval('company'))],
|
|
states=_states_done)
|
|
writeoff_description = fields.Char('Write Off Description',
|
|
states=_states_done)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._transitions |= set((
|
|
('draft', 'posted'),
|
|
('posted', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'post': {
|
|
'invisible': Eval('state') != 'draft',
|
|
},
|
|
'cancel': {
|
|
'invisible': Eval('state') != 'posted',
|
|
},
|
|
})
|
|
cls._order.insert(0, ('date', 'DESC'))
|
|
cls._order.insert(1, ('number', 'DESC'))
|
|
t = cls.__table__()
|
|
#cls._sql_indexes.update({
|
|
#Index(t, (t.state, Index.Equality())),
|
|
#})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
cursor = Transaction().connection.cursor()
|
|
table_h = cls.__table_handler__(module_name)
|
|
sql_table = cls.__table__()
|
|
super().__register__(module_name)
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.state], ['cancelled'],
|
|
where=sql_table.state == 'canceled'))
|
|
if table_h.column_exist('move_canceled'):
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.move_cancelled], [sql_table.move_canceled]))
|
|
table_h.drop_column('move_canceled')
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
def set_number(self):
|
|
pool = Pool()
|
|
FiscalYear = pool.get('account.fiscalyear')
|
|
|
|
fiscalyear_id = FiscalYear.find(self.company.id,
|
|
date=self.date)
|
|
fiscalyear = FiscalYear(fiscalyear_id)
|
|
sequence = fiscalyear.get_voucher_sequence(self.voucher_type)
|
|
if not sequence:
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_no_voucher_sequence',
|
|
voucher=self.rec_name, fiscalyear=fiscalyear.rec_name))
|
|
self.write([self], {'number': sequence.get()})
|
|
|
|
@fields.depends('currency')
|
|
def on_change_with_currency_code(self, name=None):
|
|
if self.currency:
|
|
return self.currency.code
|
|
|
|
@fields.depends('party', 'pay_lines', 'lines_credits', 'lines_debits',
|
|
'currency')
|
|
def on_change_with_amount(self, name=None):
|
|
amount = _ZERO
|
|
if self.pay_lines:
|
|
for line in self.pay_lines:
|
|
if line.pay_amount:
|
|
amount += line.pay_amount
|
|
if self.lines_credits:
|
|
for line in self.lines_credits:
|
|
if line.amount_original:
|
|
amount += line.amount_original
|
|
if self.lines_debits:
|
|
for line in self.lines_debits:
|
|
if line.amount_original:
|
|
amount += line.amount_original
|
|
return amount
|
|
|
|
@fields.depends('party', 'lines', 'currency')
|
|
def on_change_with_amount_to_pay(self, name=None):
|
|
total = 0
|
|
if self.lines:
|
|
for line in self.lines:
|
|
total += line.amount_unreconciled or _ZERO
|
|
return total
|
|
|
|
@fields.depends('party', 'lines', 'currency')
|
|
def on_change_with_amount_invoices(self, name=None):
|
|
total = Decimal('0')
|
|
if self.lines:
|
|
for line in self.lines:
|
|
total += line.amount or _ZERO
|
|
return total
|
|
|
|
@fields.depends('lines', 'currency', 'company')
|
|
def on_change_with_currency_rate(self, name=None):
|
|
if (not self.currency or not self.company or
|
|
self.currency == self.company.currency):
|
|
return None
|
|
if not self.lines:
|
|
return None
|
|
amount, amount_second_currency = Decimal(0), Decimal(0)
|
|
for line in self.lines:
|
|
amount += ((line.move_line.credit or _ZERO) +
|
|
(line.move_line.debit or _ZERO))
|
|
amount_second_currency += (line.move_line.amount_second_currency
|
|
and abs(line.move_line.amount_second_currency) or _ZERO)
|
|
if not amount or not amount_second_currency:
|
|
return None
|
|
return Decimal(amount / amount_second_currency).quantize(
|
|
Decimal(str(10 ** -6)))
|
|
|
|
@fields.depends('party', 'voucher_type', 'lines', 'lines_credits',
|
|
'lines_debits', 'currency', 'company', 'date', 'pay_invoice')
|
|
def on_change_party(self):
|
|
self.add_lines()
|
|
|
|
@fields.depends('party', 'voucher_type', 'lines', 'lines_credits',
|
|
'lines_debits', 'currency', 'company', 'date', 'pay_invoice')
|
|
def on_change_currency(self):
|
|
self.add_lines()
|
|
|
|
def add_lines(self):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
MoveLine = pool.get('account.move.line')
|
|
InvoiceAccountMoveLine = pool.get('account.invoice-account.move.line')
|
|
Currency = pool.get('currency.currency')
|
|
AccountVoucherLineCredits = pool.get('account.voucher.line.credits')
|
|
AccountVoucherLineDebits = pool.get('account.voucher.line.debits')
|
|
AccountVoucherLine = pool.get('account.voucher.line')
|
|
|
|
lines = []
|
|
lines_credits = []
|
|
lines_debits = []
|
|
|
|
if not self.currency or not self.party:
|
|
self.lines = lines
|
|
self.lines_credits = lines_credits
|
|
self.lines_debits = lines_debits
|
|
return
|
|
|
|
if self.lines:
|
|
return
|
|
|
|
second_currency = None
|
|
if self.currency != self.company.currency:
|
|
second_currency = self.currency
|
|
|
|
clause = [
|
|
('party', '=', self.party),
|
|
('state', '=', 'valid'),
|
|
('reconciliation', '=', None),
|
|
('move.state', '=', 'posted'),
|
|
]
|
|
if self.voucher_type == 'receipt':
|
|
clause.append(('account.type.receivable', '=', True))
|
|
else:
|
|
clause.append(('account.type.payable', '=', True))
|
|
|
|
if self.pay_invoice:
|
|
move_lines = self.pay_invoice.lines_to_pay
|
|
else:
|
|
move_lines = MoveLine.search(clause)
|
|
|
|
for line in move_lines:
|
|
origin = str(line.move_origin)
|
|
origin = origin[:origin.find(',')]
|
|
if origin not in [
|
|
'account.invoice',
|
|
'account.voucher',
|
|
'account.statement']:
|
|
continue
|
|
|
|
invoice = InvoiceAccountMoveLine.search([
|
|
('line', '=', line.id),
|
|
])
|
|
if invoice:
|
|
continue
|
|
|
|
if line.credit:
|
|
line_type = 'cr'
|
|
amount = line.credit
|
|
else:
|
|
amount = line.debit
|
|
line_type = 'dr'
|
|
|
|
amount_residual = abs(line.amount_residual)
|
|
currency_rate = None
|
|
if second_currency:
|
|
if line.second_currency == self.currency:
|
|
currency_rate = Decimal(
|
|
amount / abs(line.amount_second_currency)).quantize(
|
|
Decimal(str(10 ** -6)))
|
|
with Transaction().set_context(
|
|
currency_rate=currency_rate, date=self.date):
|
|
amount = Currency.compute(
|
|
self.company.currency, amount,
|
|
self.currency)
|
|
amount_residual = Currency.compute(
|
|
self.company.currency, amount_residual,
|
|
self.currency)
|
|
|
|
name = ''
|
|
model = str(line.move_origin)
|
|
invoice_date = None
|
|
if model[:model.find(',')] == 'account.invoice':
|
|
invoice = Invoice(line.move_origin.id)
|
|
invoice_date = invoice.invoice_date
|
|
if invoice.type[0:3] == 'out':
|
|
name = invoice.number
|
|
else:
|
|
name = invoice.reference
|
|
|
|
if line.credit and self.voucher_type == 'receipt':
|
|
payment_line = AccountVoucherLineCredits()
|
|
elif line.debit and self.voucher_type == 'payment':
|
|
payment_line = AccountVoucherLineDebits()
|
|
else:
|
|
payment_line = AccountVoucherLine()
|
|
payment_line.name = name
|
|
payment_line.account = line.account.id
|
|
payment_line.amount = _ZERO
|
|
payment_line.amount_original = amount
|
|
payment_line.amount_unreconciled = amount_residual
|
|
payment_line.line_type = line_type
|
|
payment_line.move_line = line.id
|
|
payment_line.date = invoice_date or line.date
|
|
payment_line.currency_rate = currency_rate
|
|
|
|
if line.credit and self.voucher_type == 'receipt':
|
|
lines_credits.append(payment_line)
|
|
elif line.debit and self.voucher_type == 'payment':
|
|
lines_debits.append(payment_line)
|
|
else:
|
|
lines.append(payment_line)
|
|
|
|
self.lines = sorted(lines, key=lambda x: x.date)
|
|
self.lines_credits = lines_credits
|
|
self.lines_debits = lines_debits
|
|
|
|
@classmethod
|
|
def delete(cls, vouchers):
|
|
if not vouchers:
|
|
return True
|
|
for voucher in vouchers:
|
|
if voucher.state != 'draft':
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_delete_voucher'))
|
|
return super().delete(vouchers)
|
|
|
|
@classmethod
|
|
def copy(cls, vouchers, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
default['number'] = None
|
|
default['date'] = cls.default_date()
|
|
default['state'] = cls.default_state()
|
|
default['lines'] = None
|
|
default['lines_credits'] = None
|
|
default['lines_debits'] = None
|
|
default['move'] = None
|
|
default['move_cancelled'] = None
|
|
return super().copy(vouchers, default=default)
|
|
|
|
def prepare_move_lines(self):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
Move = pool.get('account.move')
|
|
Invoice = pool.get('account.invoice')
|
|
Currency = pool.get('currency.currency')
|
|
|
|
# Check amount
|
|
if not self.amount > _ZERO:
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_missing_pay_lines'))
|
|
|
|
move_lines = []
|
|
line_move_ids = []
|
|
move, = Move.create([{
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'journal': self.journal.id,
|
|
'date': self.date,
|
|
'origin': str(self),
|
|
}])
|
|
self.write([self], {'move': move.id})
|
|
|
|
second_currency = None
|
|
if self.currency != self.company.currency:
|
|
second_currency = self.currency.id
|
|
|
|
#
|
|
# Pay Modes
|
|
#
|
|
if self.pay_lines:
|
|
for line in self.pay_lines:
|
|
amount = line.pay_amount
|
|
amount_second_currency = None
|
|
if second_currency:
|
|
amount_second_currency = amount
|
|
with Transaction().set_context(
|
|
currency_rate=self.currency_rate, date=self.date):
|
|
amount = Currency.compute(self.currency,
|
|
amount, self.company.currency)
|
|
|
|
if self.voucher_type == 'receipt':
|
|
if amount < _ZERO:
|
|
debit = _ZERO
|
|
credit = amount * -1
|
|
else:
|
|
debit = amount
|
|
credit = _ZERO
|
|
else:
|
|
if amount < _ZERO:
|
|
debit = amount * -1
|
|
credit = _ZERO
|
|
else:
|
|
debit = _ZERO
|
|
credit = amount
|
|
|
|
if self.voucher_type == 'payment' and second_currency:
|
|
amount_second_currency *= -1
|
|
if second_currency and amount < _ZERO:
|
|
amount_second_currency *= -1
|
|
|
|
move_lines.append({
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': line.pay_mode.account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'party': (line.pay_mode.account.party_required and
|
|
self.party.id or None),
|
|
'amount_second_currency': amount_second_currency,
|
|
'second_currency': second_currency,
|
|
})
|
|
|
|
#
|
|
# Credits
|
|
#
|
|
if self.lines_credits:
|
|
for line in self.lines_credits:
|
|
amount = line.amount_original
|
|
amount_second_currency = None
|
|
if second_currency:
|
|
amount_second_currency = amount
|
|
with Transaction().set_context(
|
|
currency_rate=line.currency_rate, date=self.date):
|
|
amount = Currency.compute(self.currency,
|
|
amount, self.company.currency)
|
|
|
|
debit = amount
|
|
credit = _ZERO
|
|
|
|
if self.voucher_type == 'payment' and second_currency:
|
|
amount_second_currency *= -1
|
|
|
|
move_lines.append({
|
|
'description': 'advance',
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': line.account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'date': self.date,
|
|
'maturity_date': self.date,
|
|
'party': (line.account.party_required and
|
|
self.party.id or None),
|
|
'amount_second_currency': amount_second_currency,
|
|
'second_currency': second_currency,
|
|
})
|
|
|
|
#
|
|
# Debits
|
|
#
|
|
if self.lines_debits:
|
|
for line in self.lines_debits:
|
|
amount = line.amount_original
|
|
amount_second_currency = None
|
|
if second_currency:
|
|
amount_second_currency = amount
|
|
with Transaction().set_context(
|
|
currency_rate=line.currency_rate, date=self.date):
|
|
amount = Currency.compute(self.currency,
|
|
amount, self.company.currency)
|
|
|
|
debit = _ZERO
|
|
credit = amount
|
|
|
|
if self.voucher_type == 'payment' and second_currency:
|
|
amount_second_currency *= -1
|
|
|
|
move_lines.append({
|
|
'description': 'advance',
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': line.account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'date': self.date,
|
|
'maturity_date': self.date,
|
|
'party': (line.account.party_required and
|
|
self.party.id or None),
|
|
'amount_second_currency': amount_second_currency,
|
|
'second_currency': second_currency,
|
|
})
|
|
|
|
#
|
|
# Voucher Lines
|
|
#
|
|
with Transaction().set_context(
|
|
currency_rate=self.currency_rate, date=self.date):
|
|
total = Currency.compute(self.currency,
|
|
self.amount, self.company.currency)
|
|
invoices = 'Factura/s: '
|
|
if self.lines:
|
|
for line in self.lines:
|
|
if line.amount > line.amount_unreconciled:
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_amount_greater_unreconciled'))
|
|
|
|
origin = str(line.move_line.move_origin)
|
|
origin = origin[:origin.find(',')]
|
|
if origin not in ('account.invoice', 'account.voucher'):
|
|
continue
|
|
if not line.amount:
|
|
continue
|
|
|
|
amount = line.amount
|
|
amount_second_currency = None
|
|
if second_currency:
|
|
amount_second_currency = amount
|
|
with Transaction().set_context(
|
|
currency_rate=line.currency_rate, date=self.date):
|
|
amount = Currency.compute(self.currency,
|
|
amount, self.company.currency)
|
|
|
|
line_move_ids.append(line.move_line)
|
|
if self.voucher_type == 'receipt':
|
|
debit = _ZERO
|
|
credit = amount
|
|
description = Invoice(line.move_line.move_origin.id).number
|
|
else:
|
|
debit = amount
|
|
credit = _ZERO
|
|
description = Invoice(
|
|
line.move_line.move_origin.id).reference
|
|
|
|
if self.voucher_type == 'receipt' and second_currency:
|
|
amount_second_currency *= -1
|
|
|
|
total -= amount
|
|
invoices += description + ', ' if description else ', '
|
|
move_lines.append({
|
|
'description': description,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': line.account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'date': self.date,
|
|
'maturity_date': self.date,
|
|
'party': (line.account.party_required and
|
|
self.party.id or None),
|
|
'amount_second_currency': amount_second_currency,
|
|
'second_currency': second_currency,
|
|
})
|
|
|
|
#
|
|
# Write Off
|
|
#
|
|
if self.writeoff and total != _ZERO:
|
|
amount = abs(total)
|
|
if self.voucher_type == 'receipt':
|
|
debit = _ZERO
|
|
credit = amount
|
|
writeoff_account = self.writeoff.debit_account
|
|
else:
|
|
debit = amount
|
|
credit = _ZERO
|
|
writeoff_account = self.writeoff.credit_account
|
|
description = '%s (%s)' % (self.number,
|
|
self.writeoff_description or self.writeoff.name)
|
|
party_required = writeoff_account.party_required
|
|
move_lines.append({
|
|
'description': description,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': writeoff_account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'date': self.date,
|
|
'maturity_date': self.date,
|
|
'party': party_required and self.party.id or None,
|
|
})
|
|
total = _ZERO
|
|
|
|
if total != _ZERO:
|
|
amount = total
|
|
amount_second_currency = None
|
|
if second_currency:
|
|
with Transaction().set_context(
|
|
currency_rate=self.currency_rate, date=self.date):
|
|
amount_second_currency = Currency.compute(
|
|
self.company.currency, amount, self.currency)
|
|
|
|
if self.voucher_type == 'receipt':
|
|
debit = _ZERO
|
|
credit = amount
|
|
account = self.party.account_receivable_used
|
|
party_required = account.party_required
|
|
else:
|
|
debit = amount
|
|
credit = _ZERO
|
|
account = self.party.account_payable_used
|
|
party_required = account.party_required
|
|
|
|
if self.voucher_type == 'receipt' and second_currency:
|
|
amount_second_currency *= -1
|
|
|
|
move_lines.append({
|
|
'description': self.number,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': account.id,
|
|
'move': move.id,
|
|
'journal': self.journal.id,
|
|
'period': Period.find(self.company.id, date=self.date),
|
|
'date': self.date,
|
|
'maturity_date': self.date,
|
|
'party': party_required and self.party.id or None,
|
|
'amount_second_currency': amount_second_currency,
|
|
'second_currency': second_currency,
|
|
})
|
|
|
|
Move.write([move], {'description': invoices[:-2]})
|
|
|
|
return move_lines
|
|
|
|
def create_move(self, move_lines):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
Invoice = pool.get('account.invoice')
|
|
Currency = pool.get('currency.currency')
|
|
|
|
created_lines = MoveLine.create(move_lines)
|
|
Move.post([self.move])
|
|
|
|
lines_to_reconcile = defaultdict(list)
|
|
payment_lines_to_relate = defaultdict(list)
|
|
|
|
for line in self.lines:
|
|
origin = str(line.move_line.move_origin)
|
|
origin = origin[:origin.find(',')]
|
|
if origin not in ['account.invoice',
|
|
'account.voucher']:
|
|
continue
|
|
if line.amount == _ZERO:
|
|
continue
|
|
|
|
invoice = Invoice(line.move_line.move_origin.id)
|
|
with Transaction().set_context(
|
|
currency_rate=self.currency_rate, date=self.date):
|
|
amount = Currency.compute(self.currency,
|
|
line.amount, self.company.currency)
|
|
|
|
reconcile_lines, remainder = \
|
|
self.get_reconcile_lines_for_amount(invoice, amount,
|
|
lines_to_reconcile[line.account.id])
|
|
|
|
if remainder == _ZERO:
|
|
for reconcile_line in reconcile_lines:
|
|
lines_to_reconcile[line.account.id].append(
|
|
reconcile_line.id)
|
|
|
|
for move_line in created_lines:
|
|
if move_line.party is None:
|
|
continue
|
|
if move_line.description == 'advance':
|
|
continue
|
|
if (move_line.debit != abs(amount) and
|
|
move_line.credit != abs(amount)):
|
|
continue
|
|
invoice_number = invoice.reference
|
|
if invoice.type == 'out':
|
|
invoice_number = invoice.number
|
|
if move_line.description == invoice_number:
|
|
if remainder == _ZERO:
|
|
lines_to_reconcile[move_line.account.id].append(
|
|
move_line.id)
|
|
payment_lines_to_relate[invoice].append(move_line.id)
|
|
|
|
if payment_lines_to_relate:
|
|
for invoice, payment_lines in payment_lines_to_relate.items():
|
|
Invoice.write([invoice], {
|
|
'payment_lines': [('add', list(set(payment_lines)))],
|
|
})
|
|
|
|
if lines_to_reconcile:
|
|
for lines_ids in lines_to_reconcile.values():
|
|
if not lines_ids:
|
|
continue
|
|
lines = MoveLine.browse(list(set(lines_ids)))
|
|
MoveLine.reconcile(lines)
|
|
|
|
reconcile_lines = []
|
|
if self.lines_credits:
|
|
for line in self.lines_credits:
|
|
reconcile_lines.append(line.move_line)
|
|
for move_line in created_lines:
|
|
if move_line.description == 'advance':
|
|
reconcile_lines.append(move_line)
|
|
if reconcile_lines:
|
|
MoveLine.reconcile(reconcile_lines)
|
|
|
|
reconcile_lines = []
|
|
if self.lines_debits:
|
|
for line in self.lines_debits:
|
|
reconcile_lines.append(line.move_line)
|
|
for move_line in created_lines:
|
|
if move_line.description == 'advance':
|
|
reconcile_lines.append(move_line)
|
|
if reconcile_lines:
|
|
MoveLine.reconcile(reconcile_lines)
|
|
|
|
return True
|
|
|
|
def get_reconcile_lines_for_amount(self, invoice, amount, reconcile_lines):
|
|
'''
|
|
Return list of lines and the remainder to make reconciliation.
|
|
'''
|
|
if self.voucher_type == 'payment':
|
|
amount = -amount
|
|
party = invoice.party
|
|
Result = namedtuple('Result', ['lines', 'remainder'])
|
|
|
|
lines = [
|
|
l for l in invoice.payment_lines + invoice.lines_to_pay
|
|
if not l.reconciliation
|
|
and (not invoice.account.party_required or l.party == party)
|
|
and (l.id not in reconcile_lines)]
|
|
|
|
best = Result([], invoice.total_amount)
|
|
for n in range(len(lines), 0, -1):
|
|
for comb_lines in combinations(lines, n):
|
|
remainder = sum((l.debit - l.credit) for l in comb_lines)
|
|
remainder -= amount
|
|
result = Result(list(comb_lines), remainder)
|
|
if invoice.currency.is_zero(remainder):
|
|
return result
|
|
if abs(remainder) < abs(best.remainder):
|
|
best = result
|
|
return best
|
|
|
|
def create_cancel_move(self):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
Period = pool.get('account.period')
|
|
Reconciliation = pool.get('account.move.reconciliation')
|
|
Invoice = pool.get('account.invoice')
|
|
PaymentLine = pool.get('account.invoice-account.move.line')
|
|
|
|
reconciliations = [x.reconciliation for x in self.move.lines
|
|
if x.reconciliation]
|
|
with Transaction().set_user(0, set_context=True):
|
|
if reconciliations:
|
|
Reconciliation.delete(reconciliations)
|
|
|
|
# Remove payment lines from their invoices.
|
|
payments = defaultdict(list)
|
|
ids = list(map(int, self.move.lines))
|
|
for sub_ids in grouped_slice(ids):
|
|
payment_lines = PaymentLine.search([
|
|
('line', 'in', list(sub_ids)),
|
|
])
|
|
for payment_line in payment_lines:
|
|
payments[payment_line.invoice].append(payment_line.line)
|
|
to_write = []
|
|
for invoice, lines in payments.items():
|
|
to_write.append([invoice])
|
|
to_write.append({'payment_lines': [('remove', lines)]})
|
|
if to_write:
|
|
Invoice.write(*to_write)
|
|
|
|
cancelled_move, = Move.copy([self.move], {
|
|
'period': Period.find(self.company.id, date=self.move.date),
|
|
'date': self.move.date,
|
|
})
|
|
self.write([self], {
|
|
'move_cancelled': cancelled_move.id,
|
|
})
|
|
|
|
for line in cancelled_move.lines:
|
|
aux = line.debit
|
|
line.debit = line.credit
|
|
line.credit = aux
|
|
line.amount_second_currency = (line.amount_second_currency * -1 if
|
|
line.amount_second_currency else _ZERO)
|
|
line.save()
|
|
|
|
Move.post([self.move_cancelled])
|
|
|
|
lines_to_reconcile = defaultdict(list)
|
|
for line in self.move.lines:
|
|
if line.account.reconcile:
|
|
lines_to_reconcile[line.account.id].append(line)
|
|
for cancel_line in cancelled_move.lines:
|
|
if cancel_line.account.reconcile:
|
|
lines_to_reconcile[cancel_line.account.id].append(cancel_line)
|
|
|
|
for lines in list(lines_to_reconcile.values()):
|
|
MoveLine.reconcile(lines)
|
|
|
|
return True
|
|
|
|
@classmethod
|
|
def check_already_reconciled(cls, vouchers):
|
|
reconciled_lines = []
|
|
for voucher in vouchers:
|
|
reconciled_lines = [l.name for l in voucher.lines
|
|
if l.move_line.reconciliation]
|
|
if reconciled_lines:
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_post_already_reconciled',
|
|
lines='\n'.join(reconciled_lines)))
|
|
|
|
@classmethod
|
|
def check_amount_invoices(cls, vouchers):
|
|
for voucher in vouchers:
|
|
if voucher.amount_invoices > voucher.amount:
|
|
raise UserError(gettext(
|
|
'account_voucher_ar.msg_amount_invoices_greater_amount'))
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('posted')
|
|
def post(cls, vouchers):
|
|
cls.check_already_reconciled(vouchers)
|
|
cls.check_amount_invoices(vouchers)
|
|
for voucher in vouchers:
|
|
voucher.set_number()
|
|
move_lines = voucher.prepare_move_lines()
|
|
voucher.create_move(move_lines)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, vouchers):
|
|
for voucher in vouchers:
|
|
voucher.create_cancel_move()
|
|
|
|
|
|
class AccountVoucherLine(ModelSQL, ModelView):
|
|
'Account Voucher Line'
|
|
__name__ = 'account.voucher.line'
|
|
|
|
_states = {'readonly': True}
|
|
|
|
voucher = fields.Many2One('account.voucher', 'Voucher',
|
|
required=True, ondelete='CASCADE')
|
|
reference = fields.Function(fields.Char('reference'),
|
|
'get_reference')
|
|
name = fields.Char('Name', states=_states)
|
|
account = fields.Many2One('account.account', 'Account',
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True)],
|
|
states=_states)
|
|
amount = fields.Numeric('Amount', digits=(16, 2))
|
|
line_type = fields.Selection([
|
|
('cr', 'Credit'),
|
|
('dr', 'Debit'),
|
|
], 'Type', states=_states)
|
|
move_line = fields.Many2One('account.move.line', 'Move Line',
|
|
states=_states)
|
|
amount_original = fields.Numeric('Original Amount',
|
|
digits=(16, 2), states=_states)
|
|
amount_unreconciled = fields.Numeric('Unreconciled amount',
|
|
digits=(16, 2), states=_states)
|
|
date = fields.Date('Date', states=_states)
|
|
date_expire = fields.Function(fields.Date('Expire date'),
|
|
'get_expire_date')
|
|
currency_rate = fields.Numeric('Currency rate', digits=(12, 6),
|
|
states=_states)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
#cls._sql_indexes.update({
|
|
#Index(t, (t.line_type, Index.Equality())),
|
|
#})
|
|
|
|
def get_reference(self, name):
|
|
Invoice = Pool().get('account.invoice')
|
|
|
|
if self.move_line.move:
|
|
invoices = Invoice.search([
|
|
('move', '=', self.move_line.move.id)])
|
|
if invoices:
|
|
return invoices[0].reference
|
|
|
|
def get_expire_date(self, name):
|
|
if self.move_line:
|
|
return self.move_line.maturity_date
|
|
|
|
|
|
class AccountVoucherLineCredits(ModelSQL, ModelView):
|
|
'Account Voucher Line Credits'
|
|
__name__ = 'account.voucher.line.credits'
|
|
|
|
_states = {'readonly': True}
|
|
|
|
voucher = fields.Many2One('account.voucher', 'Voucher',
|
|
required=True, ondelete='CASCADE')
|
|
name = fields.Char('Name')
|
|
account = fields.Many2One('account.account', 'Account',
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
],
|
|
states=_states)
|
|
amount = fields.Numeric('Amount', digits=(16, 2),
|
|
states=_states)
|
|
line_type = fields.Selection([
|
|
('cr', 'Credit'),
|
|
('dr', 'Debit'),
|
|
], 'Type', states=_states)
|
|
move_line = fields.Many2One('account.move.line', 'Move Line',
|
|
states=_states)
|
|
amount_original = fields.Numeric('Original Amount',
|
|
digits=(16, 2), states=_states)
|
|
amount_unreconciled = fields.Numeric('Unreconciled amount',
|
|
digits=(16, 2), states=_states)
|
|
date = fields.Date('Date', states=_states)
|
|
currency_rate = fields.Numeric('Currency rate', digits=(12, 6),
|
|
states=_states)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
#cls._sql_indexes.update({
|
|
#Index(t, (t.line_type, Index.Equality())),
|
|
#})
|
|
|
|
|
|
class AccountVoucherLineDebits(ModelSQL, ModelView):
|
|
'Account Voucher Line Debits'
|
|
__name__ = 'account.voucher.line.debits'
|
|
|
|
_states = {'readonly': True}
|
|
|
|
voucher = fields.Many2One('account.voucher', 'Voucher',
|
|
required=True, ondelete='CASCADE')
|
|
name = fields.Char('Name')
|
|
account = fields.Many2One('account.account', 'Account',
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
],
|
|
states=_states)
|
|
amount = fields.Numeric('Amount', digits=(16, 2),
|
|
states=_states)
|
|
line_type = fields.Selection([
|
|
('cr', 'Credit'),
|
|
('dr', 'Debit'),
|
|
], 'Type', states=_states)
|
|
move_line = fields.Many2One('account.move.line', 'Move Line',
|
|
states=_states)
|
|
amount_original = fields.Numeric('Original Amount',
|
|
digits=(16, 2), states=_states)
|
|
amount_unreconciled = fields.Numeric('Unreconciled amount',
|
|
digits=(16, 2), states=_states)
|
|
date = fields.Date('Date', states=_states)
|
|
currency_rate = fields.Numeric('Currency rate', digits=(12, 6),
|
|
states=_states)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
#cls._sql_indexes.update({
|
|
#Index(t, (t.line_type, Index.Equality())),
|
|
#})
|
|
|
|
|
|
class AccountVoucherLinePaymode(ModelSQL, ModelView):
|
|
'Account Voucher Line Pay Mode'
|
|
__name__ = 'account.voucher.line.paymode'
|
|
|
|
voucher = fields.Many2One('account.voucher', 'Voucher')
|
|
pay_mode = fields.Many2One('account.voucher.paymode', 'Pay Mode',
|
|
required=True)
|
|
pay_amount = fields.Numeric('Pay Amount', digits=(16, 2), required=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(AccountVoucherLinePaymode, cls).__setup__()
|
|
cls._buttons.update({
|
|
'calculate_remaining_amount': {
|
|
'invisible': ~Eval('_parent_voucher.state').in_(
|
|
['draft', 'calculated']),
|
|
},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_pay_amount():
|
|
return Decimal('0.0')
|
|
|
|
@classmethod
|
|
def calculate_remaining_amount(cls, paymodes):
|
|
for p in paymodes:
|
|
p.pay_amount = p.voucher.amount_invoices - p.voucher.amount
|
|
p.save()
|
|
|
|
|
|
class AccountVoucherReport(Report):
|
|
__name__ = 'account.voucher'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
report_context['company'] = report_context['user'].company
|
|
report_context['compute_currency'] = cls.compute_currency
|
|
report_context['format_vat_number'] = cls.format_vat_number
|
|
report_context['get_iva_condition'] = cls.get_iva_condition
|
|
return report_context
|
|
|
|
@classmethod
|
|
def compute_currency(cls, voucher_currency, amount_original,
|
|
company_currency):
|
|
return voucher_currency.compute(company_currency, amount_original,
|
|
voucher_currency)
|
|
|
|
@classmethod
|
|
def format_vat_number(cls, vat_number=''):
|
|
return '%s-%s-%s' % (vat_number[:2], vat_number[2:-1], vat_number[-1])
|
|
|
|
@classmethod
|
|
def get_iva_condition(cls, party):
|
|
return dict(party._fields['iva_condition'].selection)[
|
|
party.iva_condition]
|