The payable or receivable account may not have party required set but it should still be allowed to register payment their lines. issue6787 review39701002
389 lines
13 KiB
Python
389 lines
13 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 itertools import groupby
|
|
|
|
from trytond.model import Workflow, ModelView, ModelSQL, fields
|
|
from trytond.pyson import Eval, If
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Wizard, StateView, StateAction, Button
|
|
from trytond.pool import Pool
|
|
from trytond import backend
|
|
|
|
__all__ = ['Journal', 'Group', 'Payment',
|
|
'ProcessPaymentStart', 'ProcessPayment']
|
|
|
|
KINDS = [
|
|
('payable', 'Payable'),
|
|
('receivable', 'Receivable'),
|
|
]
|
|
|
|
|
|
class Journal(ModelSQL, ModelView):
|
|
'Payment Journal'
|
|
__name__ = 'account.payment.journal'
|
|
name = fields.Char('Name', required=True)
|
|
currency = fields.Many2One('currency.currency', 'Currency', required=True)
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
select=True)
|
|
process_method = fields.Selection([
|
|
('manual', 'Manual'),
|
|
], 'Process Method', required=True)
|
|
|
|
@staticmethod
|
|
def default_currency():
|
|
if Transaction().context.get('company'):
|
|
Company = Pool().get('company.company')
|
|
company = Company(Transaction().context['company'])
|
|
return company.currency.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class Group(ModelSQL, ModelView):
|
|
'Payment Group'
|
|
__name__ = 'account.payment.group'
|
|
_rec_name = 'number'
|
|
number = fields.Char('Number', required=True, readonly=True)
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
readonly=True, select=True, domain=[
|
|
('id', If(Eval('context', {}).contains('company'), '=', '!='),
|
|
Eval('context', {}).get('company', -1)),
|
|
])
|
|
journal = fields.Many2One('account.payment.journal', 'Journal',
|
|
required=True, readonly=True, domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['company'])
|
|
kind = fields.Selection(KINDS, 'Kind', required=True, readonly=True)
|
|
payments = fields.One2Many('account.payment', 'group', 'Payments',
|
|
readonly=True)
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
table_h = TableHandler(cls, module_name)
|
|
|
|
# Migration from 3.8: rename reference into number
|
|
if table_h.column_exist('reference'):
|
|
table_h.column_rename('reference', 'number')
|
|
super(Group, cls).__register__(module_name)
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
Sequence = pool.get('ir.sequence')
|
|
Config = pool.get('account.configuration')
|
|
|
|
vlist = [v.copy() for v in vlist]
|
|
config = Config(1)
|
|
for values in vlist:
|
|
if values.get('number') is None:
|
|
values['number'] = Sequence.get_id(
|
|
config.payment_group_sequence.id)
|
|
|
|
return super(Group, cls).create(vlist)
|
|
|
|
@classmethod
|
|
def copy(cls, groups, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('payments', None)
|
|
return super(Group, cls).copy(groups, default=default)
|
|
|
|
_STATES = {
|
|
'readonly': Eval('state') != 'draft',
|
|
}
|
|
_DEPENDS = ['state']
|
|
|
|
|
|
class Payment(Workflow, ModelSQL, ModelView):
|
|
'Payment'
|
|
__name__ = 'account.payment'
|
|
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.payment.journal', 'Journal',
|
|
required=True, states=_STATES, domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=_DEPENDS + ['company'])
|
|
currency = fields.Function(fields.Many2One('currency.currency',
|
|
'Currency'), 'on_change_with_currency',
|
|
searcher='search_currency')
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
|
'on_change_with_currency_digits')
|
|
kind = fields.Selection(KINDS, 'Kind', required=True,
|
|
states=_STATES, depends=_DEPENDS)
|
|
party = fields.Many2One('party.party', 'Party', required=True,
|
|
states=_STATES, depends=_DEPENDS)
|
|
date = fields.Date('Date', required=True, states=_STATES, depends=_DEPENDS)
|
|
amount = fields.Numeric('Amount', required=True,
|
|
digits=(16, Eval('currency_digits', 2)), states=_STATES,
|
|
depends=_DEPENDS + ['currency_digits'])
|
|
line = fields.Many2One('account.move.line', 'Line', ondelete='RESTRICT',
|
|
domain=[
|
|
('move.company', '=', Eval('company', -1)),
|
|
If(Eval('kind') == 'receivable',
|
|
['OR', ('debit', '>', 0), ('credit', '<', 0)],
|
|
['OR', ('credit', '>', 0), ('debit', '<', 0)],
|
|
),
|
|
('account.kind', 'in', ['receivable', 'payable']),
|
|
('party', 'in', [Eval('party', None), None]),
|
|
If(Eval('state') == 'draft',
|
|
[
|
|
('reconciliation', '=', None),
|
|
],
|
|
[]),
|
|
['OR',
|
|
('second_currency', '=', Eval('currency', None)),
|
|
[
|
|
('account.company.currency', '=', Eval('currency', None)),
|
|
('second_currency', '=', None),
|
|
],
|
|
],
|
|
('move_state', '=', 'posted'),
|
|
],
|
|
states=_STATES, depends=_DEPENDS + ['party', 'currency', 'kind',
|
|
'company'])
|
|
description = fields.Char('Description', states=_STATES, depends=_DEPENDS)
|
|
origin = fields.Reference(
|
|
"Origin", selection='get_origin', select=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
depends=['state'])
|
|
group = fields.Many2One('account.payment.group', 'Group', readonly=True,
|
|
ondelete='RESTRICT',
|
|
states={
|
|
'required': Eval('state').in_(['processing', 'succeeded']),
|
|
},
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['state', 'company'])
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('approved', 'Approved'),
|
|
('processing', 'Processing'),
|
|
('succeeded', 'Succeeded'),
|
|
('failed', 'Failed'),
|
|
], 'State', readonly=True, select=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Payment, cls).__setup__()
|
|
cls._order.insert(0, ('date', 'DESC'))
|
|
cls._error_messages.update({
|
|
'delete_draft': ('Payment "%s" must be in draft before '
|
|
'deletion.'),
|
|
})
|
|
cls._transitions |= set((
|
|
('draft', 'approved'),
|
|
('approved', 'processing'),
|
|
('processing', 'succeeded'),
|
|
('processing', 'failed'),
|
|
('approved', 'draft'),
|
|
('succeeded', 'failed'),
|
|
('failed', 'succeeded'),
|
|
))
|
|
cls._buttons.update({
|
|
'draft': {
|
|
'invisible': Eval('state') != 'approved',
|
|
'icon': 'tryton-go-previous',
|
|
},
|
|
'approve': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'icon': 'tryton-go-next',
|
|
},
|
|
'succeed': {
|
|
'invisible': ~Eval('state').in_(
|
|
['processing', 'failed']),
|
|
'icon': 'tryton-ok',
|
|
},
|
|
'fail': {
|
|
'invisible': ~Eval('state').in_(
|
|
['processing', 'succeeded']),
|
|
'icon': 'tryton-cancel',
|
|
},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_kind():
|
|
return 'payable'
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
return Date.today()
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@fields.depends('journal')
|
|
def on_change_with_currency(self, name=None):
|
|
if self.journal:
|
|
return self.journal.currency.id
|
|
|
|
@fields.depends('journal')
|
|
def on_change_with_currency_digits(self, name=None):
|
|
if self.journal:
|
|
return self.journal.currency.digits
|
|
return 2
|
|
|
|
@classmethod
|
|
def search_currency(cls, name, clause):
|
|
return [('journal.' + clause[0],) + tuple(clause[1:])]
|
|
|
|
@fields.depends('kind')
|
|
def on_change_kind(self):
|
|
self.line = None
|
|
|
|
@fields.depends('party')
|
|
def on_change_party(self):
|
|
self.line = None
|
|
|
|
@fields.depends('line')
|
|
def on_change_line(self):
|
|
if self.line:
|
|
self.date = self.line.maturity_date
|
|
self.amount = self.line.payment_amount
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
'Return list of Model names for origin Reference'
|
|
return []
|
|
|
|
@classmethod
|
|
def get_origin(cls):
|
|
Model = Pool().get('ir.model')
|
|
models = cls._get_origin()
|
|
models = Model.search([
|
|
('model', 'in', models),
|
|
])
|
|
return [(None, '')] + [(m.model, m.name) for m in models]
|
|
|
|
@classmethod
|
|
def delete(cls, payments):
|
|
for payment in payments:
|
|
if payment.state != 'draft':
|
|
cls.raise_user_error('delete_draft', (payment.rec_name))
|
|
super(Payment, cls).delete(payments)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, payments):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('approved')
|
|
def approve(cls, payments):
|
|
pass
|
|
|
|
@classmethod
|
|
@Workflow.transition('processing')
|
|
def process(cls, payments, group):
|
|
pool = Pool()
|
|
Group = pool.get('account.payment.group')
|
|
if payments:
|
|
group = group()
|
|
cls.write(payments, {
|
|
'group': group.id,
|
|
})
|
|
process_method = getattr(Group,
|
|
'process_%s' % group.journal.process_method, None)
|
|
if process_method:
|
|
process_method(group)
|
|
group.save()
|
|
return group
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('succeeded')
|
|
def succeed(cls, payments):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('failed')
|
|
def fail(cls, payments):
|
|
pass
|
|
|
|
|
|
class ProcessPaymentStart(ModelView):
|
|
'Process Payment'
|
|
__name__ = 'account.payment.process.start'
|
|
|
|
|
|
class ProcessPayment(Wizard):
|
|
'Process Payment'
|
|
__name__ = 'account.payment.process'
|
|
start = StateView('account.payment.process.start',
|
|
'account_payment.payment_process_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Process', 'process', 'tryton-ok', default=True),
|
|
])
|
|
process = StateAction('account_payment.act_payment_group_form')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ProcessPayment, cls).__setup__()
|
|
cls._error_messages.update({
|
|
'overpay': 'The Payment "%(payment)s" overpays '
|
|
'the Line "%(line)s".',
|
|
})
|
|
|
|
def _group_payment_key(self, payment):
|
|
return (('journal', payment.journal.id), ('kind', payment.kind))
|
|
|
|
def _new_group(self, values):
|
|
pool = Pool()
|
|
Group = pool.get('account.payment.group')
|
|
return Group(**values)
|
|
|
|
def do_process(self, action):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
payments = Payment.browse(Transaction().context['active_ids'])
|
|
|
|
for payment in payments:
|
|
if payment.line and payment.line.payment_amount < 0:
|
|
self.raise_user_warning(str(payment),
|
|
'overpay', {
|
|
'payment': payment.rec_name,
|
|
'line': payment.line.rec_name,
|
|
})
|
|
|
|
groups = []
|
|
payments = sorted(payments, key=self._group_payment_key)
|
|
for key, grouped_payments in groupby(payments,
|
|
key=self._group_payment_key):
|
|
def group():
|
|
group = self._new_group(dict(key))
|
|
group.save()
|
|
groups.append(group)
|
|
return group
|
|
Payment.process(list(grouped_payments), group)
|
|
|
|
return action, {
|
|
'res_id': [g.id for g in groups],
|
|
}
|