Files
tryton/modules/account_payment/payment.py
T

703 lines
24 KiB
Python
Raw Normal View History

2014-12-02 12:54:44 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from collections import defaultdict
from decimal import Decimal
2014-03-10 23:06:29 +01:00
from itertools import groupby
2021-12-11 17:58:41 +01:00
from sql.aggregate import Count, Sum
from sql.functions import CharLength
from sql.operators import Abs
2018-12-29 14:20:28 +01:00
from trytond.i18n import gettext
2020-09-12 18:32:14 +02:00
from trytond.model import (
DeactivableMixin, Index, ModelSQL, ModelView, Workflow, fields)
2018-12-29 14:20:28 +01:00
from trytond.model.exceptions import AccessError
2021-12-11 17:58:41 +01:00
from trytond.modules.company.model import (
employee_field, reset_employee, set_employee)
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool
2014-03-10 23:06:29 +01:00
from trytond.pyson import Eval, If
2018-08-26 18:07:21 +02:00
from trytond.rpc import RPC
2021-12-11 17:58:41 +01:00
from trytond.tools import (
cursor_dict, grouped_slice, reduce_ids, sortable_values)
2014-03-10 23:06:29 +01:00
from trytond.transaction import Transaction
from trytond.wizard import StateAction, Wizard
2021-08-21 09:09:55 +02:00
from .exceptions import OverpayWarning, ReconciledWarning
2018-12-29 14:20:28 +01:00
2014-03-10 23:06:29 +01:00
KINDS = [
('payable', 'Payable'),
('receivable', 'Receivable'),
]
2020-09-12 18:32:14 +02:00
class Journal(DeactivableMixin, ModelSQL, ModelView):
2014-03-10 23:06:29 +01:00
'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)
2014-03-10 23:06:29 +01:00
process_method = fields.Selection([
('manual', 'Manual'),
], 'Process Method', required=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('name', 'ASC'))
2014-03-10 23:06:29 +01:00
@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)
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company",
required=True, readonly=True)
2014-03-10 23:06:29 +01:00
journal = fields.Many2One('account.payment.journal', 'Journal',
required=True, readonly=True, domain=[
('company', '=', Eval('company', -1)),
])
2014-03-10 23:06:29 +01:00
kind = fields.Selection(KINDS, 'Kind', required=True, readonly=True)
2021-04-20 09:18:58 +02:00
payments = fields.One2Many(
'account.payment', 'group', 'Payments', readonly=True,
domain=[
('company', '=', Eval('company', -1)),
2021-10-11 18:22:19 +02:00
('journal', '=', Eval('journal', -1)),
])
2021-08-21 09:09:55 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"),
'on_change_with_currency', searcher='search_currency')
payment_count = fields.Function(fields.Integer(
"Payment Count",
help="The number of payments in the group."),
'get_payment_aggregated')
2021-08-21 09:09:55 +02:00
payment_amount = fields.Function(Monetary(
"Payment Total Amount", currency='currency', digits='currency',
help="The sum of all payment amounts."),
'get_payment_aggregated')
2021-08-21 09:09:55 +02:00
payment_amount_succeeded = fields.Function(Monetary(
"Payment Succeeded", currency='currency', digits='currency',
help="The sum of the amounts of the successful payments."),
'get_payment_aggregated')
payment_complete = fields.Function(fields.Boolean(
"Payment Complete",
help="All the payments in the group are complete."),
'get_payment_aggregated', searcher='search_complete')
2014-03-10 23:06:29 +01:00
2021-10-11 18:22:19 +02:00
process_method = fields.Function(
fields.Selection('get_process_methods', "Process Method"),
'on_change_with_process_method', searcher='search_process_method')
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update(
succeed={
'invisible': Eval('payment_complete', False),
'depends': ['payment_complete', 'process_method'],
},
)
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
2014-03-10 23:06:29 +01:00
@staticmethod
def default_company():
return Transaction().context.get('company')
2021-10-11 18:22:19 +02:00
@classmethod
def get_process_methods(cls):
pool = Pool()
Journal = pool.get('account.payment.journal')
field_name = 'process_method'
return Journal.fields_get([field_name])[field_name]['selection']
@fields.depends('journal')
def on_change_with_process_method(self, name=None):
if self.journal:
return self.journal.process_method
@classmethod
def search_process_method(cls, name, clause):
return [('journal.' + clause[0],) + tuple(clause[1:])]
2014-03-10 23:06:29 +01:00
@classmethod
def create(cls, vlist):
pool = Pool()
Config = pool.get('account.configuration')
2014-03-10 23:06:29 +01:00
vlist = [v.copy() for v in vlist]
config = Config(1)
2021-04-03 14:27:10 +02:00
default_company = cls.default_company()
2014-03-10 23:06:29 +01:00
for values in vlist:
if values.get('number') is None:
2021-04-03 14:27:10 +02:00
values['number'] = config.get_multivalue(
'payment_group_sequence',
company=values.get('company', default_company)).get()
2014-03-10 23:06:29 +01:00
return super(Group, cls).create(vlist)
@classmethod
def copy(cls, groups, default=None):
if default is None:
default = {}
else:
default = default.copy()
2022-01-19 16:19:18 +01:00
default.setdefault('number', None)
2014-03-10 23:06:29 +01:00
default.setdefault('payments', None)
return super(Group, cls).copy(groups, default=default)
2021-10-11 18:22:19 +02:00
@classmethod
@ModelView.button
def succeed(cls, groups):
pool = Pool()
Payment = pool.get('account.payment')
payments = sum((g.payments for g in groups), ())
Payment.succeed(payments)
@classmethod
def _get_complete_states(cls):
return ['succeeded', 'failed']
@classmethod
def get_payment_aggregated(cls, groups, names):
pool = Pool()
Payment = pool.get('account.payment')
cursor = Transaction().connection.cursor()
payment = Payment.__table__()
# initialize result and columns
result = defaultdict(lambda: defaultdict(lambda: None))
columns = [
payment.group.as_('group_id'),
Count(payment.group).as_('payment_count'),
Sum(payment.amount).as_('payment_amount'),
Sum(payment.amount,
filter_=(payment.state == 'succeeded'),
).as_('payment_amount_succeeded'),
Count(payment.group,
filter_=(~payment.state.in_(cls._get_complete_states())),
).as_('payment_not_complete'),
]
for sub_ids in grouped_slice(groups):
cursor.execute(*payment.select(*columns,
where=reduce_ids(payment.group, sub_ids),
group_by=payment.group),
)
for row in cursor_dict(cursor):
group_id = row['group_id']
result['payment_count'][group_id] = row['payment_count']
result['payment_complete'][group_id] = \
not row['payment_not_complete']
amount = row['payment_amount']
succeeded = row['payment_amount_succeeded']
if amount is not None:
# SQLite uses float for SUM
if not isinstance(amount, Decimal):
amount = Decimal(str(amount))
amount = cls(group_id).company.currency.round(amount)
result['payment_amount'][group_id] = amount
if succeeded is not None:
# SQLite uses float for SUM
if not isinstance(succeeded, Decimal):
succeeded = Decimal(str(succeeded))
succeeded = cls(group_id).company.currency.round(succeeded)
result['payment_amount_succeeded'][group_id] = succeeded
for key in list(result.keys()):
if key not in names:
del result[key]
return result
@classmethod
def search_complete(cls, name, clause):
pool = Pool()
Payment = pool.get('account.payment')
payment = Payment.__table__()
query_not_completed = payment.select(payment.group,
where=~payment.state.in_(cls._get_complete_states()),
group_by=payment.group)
operators = {
'=': 'not in',
'!=': 'in',
}
reverse = {
'=': 'in',
'!=': 'not in',
}
if clause[1] in operators:
if clause[2]:
return [('id', operators[clause[1]], query_not_completed)]
else:
return [('id', reverse[clause[1]], query_not_completed)]
else:
return []
@fields.depends('journal')
2021-08-21 09:09:55 +02:00
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.journal.currency if self.journal else None
@classmethod
def search_currency(cls, name, clause):
return [('journal.' + clause[0],) + tuple(clause[1:])]
2020-03-01 12:33:51 +01:00
2014-03-10 23:06:29 +01:00
_STATES = {
'readonly': Eval('state') != 'draft',
}
class Payment(Workflow, ModelSQL, ModelView):
'Payment'
__name__ = 'account.payment'
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company", required=True, states=_STATES)
2014-03-10 23:06:29 +01:00
journal = fields.Many2One('account.payment.journal', 'Journal',
required=True, states=_STATES, domain=[
('company', '=', Eval('company', -1)),
])
2014-03-10 23:06:29 +01:00
currency = fields.Function(fields.Many2One('currency.currency',
2017-05-30 23:53:18 +02:00
'Currency'), 'on_change_with_currency',
searcher='search_currency')
2014-03-10 23:06:29 +01:00
kind = fields.Selection(KINDS, 'Kind', required=True,
states=_STATES)
party = fields.Many2One(
'party.party', "Party", required=True, states=_STATES,
context={
'company': Eval('company', -1),
},
depends={'company'})
date = fields.Date('Date', required=True, states=_STATES)
2021-08-21 09:09:55 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency', required=True,
states={
'readonly': ~Eval('state').in_(
If(Eval('process_method') == 'manual',
['draft', 'processing'],
['draft'])),
})
2014-03-10 23:06:29 +01:00
line = fields.Many2One('account.move.line', 'Line', ondelete='RESTRICT',
domain=[
2014-12-03 16:13:20 +01:00
('move.company', '=', Eval('company', -1)),
2014-03-10 23:06:29 +01:00
If(Eval('kind') == 'receivable',
['OR', ('debit', '>', 0), ('credit', '<', 0)],
['OR', ('credit', '>', 0), ('debit', '<', 0)],
),
2019-03-24 18:00:01 +01:00
['OR',
('account.type.receivable', '=', True),
('account.type.payable', '=', True),
],
2017-10-02 16:03:01 +02:00
('party', 'in', [Eval('party', None), None]),
2014-03-10 23:06:29 +01:00
If(Eval('state') == 'draft',
[
('reconciliation', '=', None),
('maturity_date', '!=', None),
2014-03-10 23:06:29 +01:00
],
[]),
['OR',
('second_currency', '=', Eval('currency', None)),
[
('account.company.currency', '=', Eval('currency', None)),
('second_currency', '=', None),
],
],
('move_state', '=', 'posted'),
],
states=_STATES)
description = fields.Char('Description', states=_STATES)
2017-05-30 23:53:18 +02:00
origin = fields.Reference(
"Origin", selection='get_origin',
2017-05-30 23:53:18 +02:00
states={
'readonly': Eval('state') != 'draft',
})
2014-03-10 23:06:29 +01:00
group = fields.Many2One('account.payment.group', 'Group', readonly=True,
ondelete='RESTRICT',
states={
'required': Eval('state').in_(['processing', 'succeeded']),
},
domain=[
('company', '=', Eval('company', -1)),
])
2020-02-17 11:18:17 +01:00
process_method = fields.Function(
fields.Selection('get_process_methods', "Process Method"),
2020-04-11 00:22:19 +02:00
'on_change_with_process_method', searcher='search_process_method')
2022-04-11 20:41:56 +02:00
submitted_by = employee_field(
"Submitted by",
states=['submitted', 'processing', 'succeeded', 'failed'])
2021-04-11 10:48:26 +02:00
approved_by = employee_field(
"Approved by",
states=['approved', 'processing', 'succeeded', 'failed'])
succeeded_by = employee_field(
"Success Noted by", states=['succeeded', 'processing'])
failed_by = employee_field(
"Failure Noted by",
states=['failed', 'processing'])
2014-03-10 23:06:29 +01:00
state = fields.Selection([
('draft', 'Draft'),
2022-04-11 20:41:56 +02:00
('submitted', "Submitted"),
2014-03-10 23:06:29 +01:00
('approved', 'Approved'),
('processing', 'Processing'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
], "State", readonly=True, sort=False,
2022-04-11 20:41:56 +02:00
domain=[
If(Eval('kind') == 'receivable',
('state', '!=', 'approved'),
()),
])
2014-03-10 23:06:29 +01:00
@property
def amount_line_paid(self):
if self.state != 'failed':
if self.line.second_currency:
payment_amount = abs(self.line.amount_second_currency)
else:
payment_amount = abs(self.line.credit - self.line.debit)
return max(min(self.amount, payment_amount), 0)
return Decimal(0)
2014-03-10 23:06:29 +01:00
@classmethod
def __setup__(cls):
super(Payment, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.add(
Index(
t, (t.state, Index.Equality()),
where=t.state.in_([
'draft', 'submitted', 'approved', 'processing'])))
2014-03-10 23:06:29 +01:00
cls._order.insert(0, ('date', 'DESC'))
cls._transitions |= set((
2022-04-11 20:41:56 +02:00
('draft', 'submitted'),
('submitted', 'approved'),
('submitted', 'processing'),
2014-03-10 23:06:29 +01:00
('approved', 'processing'),
('processing', 'succeeded'),
('processing', 'failed'),
2022-04-11 20:41:56 +02:00
('submitted', 'draft'),
2014-03-10 23:06:29 +01:00
('approved', 'draft'),
('succeeded', 'failed'),
2020-08-29 18:17:51 +02:00
('succeeded', 'processing'),
('failed', 'succeeded'),
2020-08-29 18:17:51 +02:00
('failed', 'processing'),
2014-03-10 23:06:29 +01:00
))
cls._buttons.update({
'draft': {
2022-04-11 20:41:56 +02:00
'invisible': ~Eval('state').in_(['submitted', 'approved']),
2018-08-19 20:02:58 +02:00
'icon': 'tryton-back',
2018-02-19 11:37:31 +01:00
'depends': ['state'],
2014-03-10 23:06:29 +01:00
},
2022-04-11 20:41:56 +02:00
'submit': {
2014-03-10 23:06:29 +01:00
'invisible': Eval('state') != 'draft',
2018-08-19 20:02:58 +02:00
'icon': 'tryton-forward',
2018-02-19 11:37:31 +01:00
'depends': ['state'],
2014-03-10 23:06:29 +01:00
},
2022-04-11 20:41:56 +02:00
'approve': {
'invisible': (
(Eval('state') != 'submitted')
| (Eval('kind') == 'receivable')),
'icon': 'tryton-forward',
'depends': ['state', 'kind'],
},
'process_wizard': {
'invisible': ~(
(Eval('state') == 'approved')
| ((Eval('state') == 'submitted')
& (Eval('kind') == 'receivable'))),
'icon': 'tryton-launch',
'depends': ['state', 'kind'],
},
'proceed': {
'invisible': (
~Eval('state').in_(['succeeded', 'failed'])
| (Eval('process_method') != 'manual')),
'icon': 'tryton-back',
'depends': ['state', 'process_method'],
},
2014-03-10 23:06:29 +01:00
'succeed': {
'invisible': ~Eval('state').in_(
['processing', 'failed']),
2014-03-10 23:06:29 +01:00
'icon': 'tryton-ok',
2018-02-19 11:37:31 +01:00
'depends': ['state'],
2014-03-10 23:06:29 +01:00
},
'fail': {
'invisible': ~Eval('state').in_(
['processing', 'succeeded']),
2014-03-10 23:06:29 +01:00
'icon': 'tryton-cancel',
2018-02-19 11:37:31 +01:00
'depends': ['state'],
2014-03-10 23:06:29 +01:00
},
})
2018-08-26 18:07:21 +02:00
cls.__rpc__.update({
'approve': RPC(
readonly=False, instantiate=0, fresh_session=True),
})
2014-03-10 23:06:29 +01:00
@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):
2023-03-26 09:49:00 +02:00
return self.journal.currency if self.journal else None
2014-03-10 23:06:29 +01:00
2017-05-30 23:53:18 +02:00
@classmethod
def search_currency(cls, name, clause):
2017-09-27 18:37:25 +02:00
return [('journal.' + clause[0],) + tuple(clause[1:])]
2017-05-30 23:53:18 +02:00
@classmethod
def order_amount(cls, tables):
table, _ = tables[None]
context = Transaction().context
column = cls.amount.sql_column(table)
if isinstance(context.get('amount_order'), Decimal):
return [Abs(column - abs(context['amount_order']))]
else:
return [column]
2014-03-10 23:06:29 +01:00
@fields.depends('kind')
def on_change_kind(self):
2014-10-29 20:39:17 +01:00
self.line = None
2014-03-10 23:06:29 +01:00
@fields.depends('party')
def on_change_party(self):
2014-10-29 20:39:17 +01:00
self.line = None
2014-03-10 23:06:29 +01:00
2019-10-16 21:18:12 +02:00
@fields.depends('line',
'_parent_line.maturity_date', '_parent_line.payment_amount')
2014-03-10 23:06:29 +01:00
def on_change_line(self):
if self.line:
2014-10-29 20:39:17 +01:00
self.date = self.line.maturity_date
self.amount = self.line.payment_amount
2014-03-10 23:06:29 +01:00
2017-05-30 23:53:18 +02:00
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
return []
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = Model.get_name
2017-05-30 23:53:18 +02:00
models = cls._get_origin()
2020-12-27 00:19:30 +01:00
return [(None, '')] + [(m, get_name(m)) for m in models]
2017-05-30 23:53:18 +02:00
2020-02-17 11:18:17 +01:00
@fields.depends('journal')
def on_change_with_process_method(self, name=None):
if self.journal:
return self.journal.process_method
2020-04-11 00:22:19 +02:00
@classmethod
def search_process_method(cls, name, clause):
return [('journal.' + clause[0],) + tuple(clause[1:])]
2020-02-17 11:18:17 +01:00
@classmethod
def get_process_methods(cls):
pool = Pool()
Journal = pool.get('account.payment.journal')
field_name = 'process_method'
return Journal.fields_get([field_name])[field_name]['selection']
@classmethod
def view_attributes(cls):
context = Transaction().context
attributes = super().view_attributes()
if context.get('kind') == 'receivable':
attributes.append(
('/tree//button[@name="approve"]', 'tree_invisible', True))
return attributes
2021-03-10 11:43:34 +01:00
@classmethod
def copy(cls, payments, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('group', None)
2021-04-11 10:48:26 +02:00
default.setdefault('approved_by')
default.setdefault('succeeded_by')
default.setdefault('failed_by')
2021-03-10 11:43:34 +01:00
return super().copy(payments, default=default)
2014-03-10 23:06:29 +01:00
@classmethod
def delete(cls, payments):
for payment in payments:
if payment.state != 'draft':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account_payment.msg_payment_delete_draft',
payment=payment.rec_name))
2014-03-10 23:06:29 +01:00
super(Payment, cls).delete(payments)
@classmethod
@ModelView.button
@Workflow.transition('draft')
2022-04-11 20:41:56 +02:00
@reset_employee('submitted_by', 'approved_by', 'succeeded_by', 'failed_by')
2014-03-10 23:06:29 +01:00
def draft(cls, payments):
pass
2022-04-11 20:41:56 +02:00
@classmethod
@ModelView.button
@Workflow.transition('submitted')
@set_employee('submitted_by')
def submit(cls, payments):
cls._check_reconciled(payments)
2022-04-11 20:41:56 +02:00
2014-03-10 23:06:29 +01:00
@classmethod
@ModelView.button
@Workflow.transition('approved')
2021-04-11 10:48:26 +02:00
@set_employee('approved_by')
2014-03-10 23:06:29 +01:00
def approve(cls, payments):
cls._check_reconciled(payments)
2014-03-10 23:06:29 +01:00
@classmethod
@ModelView.button_action('account_payment.act_process_payments')
def process_wizard(cls, payments):
pass
2014-03-10 23:06:29 +01:00
@classmethod
2020-10-22 00:03:07 +02:00
@Workflow.transition('processing')
2014-03-10 23:06:29 +01:00
def process(cls, payments, group):
pool = Pool()
Group = pool.get('account.payment.group')
if payments:
group = group()
cls.write(payments, {
'group': group.id,
})
2020-08-29 18:17:51 +02:00
# Set state before calling process method
# as it may set a different state directly
cls.proceed(payments)
2014-03-10 23:06:29 +01:00
process_method = getattr(Group,
'process_%s' % group.journal.process_method, None)
if process_method:
process_method(group)
group.save()
return group
2020-08-29 18:17:51 +02:00
@classmethod
@ModelView.button
2020-08-29 18:17:51 +02:00
@Workflow.transition('processing')
def proceed(cls, payments):
assert all(p.group for p in payments)
cls._check_reconciled(
[p for p in payments if p.state not in {'succeeded', 'failed'}])
2020-08-29 18:17:51 +02:00
2014-03-10 23:06:29 +01:00
@classmethod
@ModelView.button
@Workflow.transition('succeeded')
2021-04-11 10:48:26 +02:00
@set_employee('succeeded_by')
2014-03-10 23:06:29 +01:00
def succeed(cls, payments):
pass
@classmethod
@ModelView.button
@Workflow.transition('failed')
2021-04-11 10:48:26 +02:00
@set_employee('failed_by')
2014-03-10 23:06:29 +01:00
def fail(cls, payments):
pass
@classmethod
def _check_reconciled(cls, payments):
pool = Pool()
Warning = pool.get('res.user.warning')
for payment in payments:
if payment.line and payment.line.reconciliation:
key = Warning.format('submit_reconciled', [payment])
if Warning.check(key):
raise ReconciledWarning(
key, gettext(
'account_payment.msg_payment_reconciled',
payment=payment.rec_name,
line=payment.line.rec_name))
2014-03-10 23:06:29 +01:00
class ProcessPayment(Wizard):
'Process Payment'
__name__ = 'account.payment.process'
start_state = 'process'
2014-03-10 23:06:29 +01:00
process = StateAction('account_payment.act_payment_group_form')
def _group_payment_key(self, payment):
2022-01-30 01:21:05 +01:00
return (
('company', payment.company.id),
('journal', payment.journal.id),
('kind', payment.kind),
)
2014-03-10 23:06:29 +01:00
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')
2018-12-29 14:20:28 +01:00
Warning = pool.get('res.user.warning')
payments = self.records
2014-03-10 23:06:29 +01:00
2022-04-11 20:41:56 +02:00
payments = [
p for p in payments
if p.state == 'approved'
or (p.state == 'submitted' and p.kind == 'receivable')]
2014-03-10 23:06:29 +01:00
for payment in payments:
if payment.line and payment.line.payment_amount < 0:
2018-12-29 14:20:28 +01:00
if Warning.check(str(payment)):
raise OverpayWarning(str(payment),
gettext('account_payment.msg_payment_overpay',
payment=payment.rec_name,
line=payment.line.rec_name))
2014-03-10 23:06:29 +01:00
groups = []
2020-07-21 00:36:40 +02:00
payments = sorted(
payments, key=sortable_values(self._group_payment_key))
2014-03-10 23:06:29 +01:00
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],
}