Add group lines wizard

issue8213
review275091002
This commit is contained in:
Cédric Krier committed 2019-04-12 19:43:53 +02:00
1 parent c9fcc894ce
commit aeabe70531
12 files changed
+403 -5

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
* Add group lines wizard
* Manage delegate reconciliation
* Remove starting wildcard when searching on codes and numbers
* Remove display balance
* Merge account kind and account type
+2
View File
@@ -55,6 +55,7 @@ def register():
ReconcileLinesWriteOff,
ReconcileShow,
CancelMovesDefault,
GroupLinesStart,
PrintGeneralJournalStart,
tax.TaxGroup,
tax.TaxCodeTemplate,
@@ -93,6 +94,7 @@ def register():
UnreconcileLines,
Reconcile,
CancelMoves,
GroupLines,
PrintGeneralJournal,
CreateMove,
tax.OpenChartTaxCode,
+3
View File
@@ -177,6 +177,9 @@ possible. The configuration `reconciliation_chunk` in `account` section allow
to define the length of lines that is allowed to search for proposal. The
default is 10.
The *Group Lines* wizard allow to group payable and/or receivable lines in
order to have a unique payable or receivable line.
Tax Code
********
+8
View File
@@ -75,3 +75,11 @@ class CancelWarning(UserWarning):
class ReconciliationError(ValidationError):
pass
class DeleteDelegatedWarning(UserWarning):
pass
class GroupLineError(UserError):
pass
+23
View File
@@ -124,6 +124,10 @@ Use the current period to cancel the move?</field>
<record model="ir.message" id="msg_write_reconciliation">
<field name="text">You cannot modify any reconciliation.</field>
</record>
<record model="ir.message" id="msg_reconciliation_delete_delegated">
<field name="text">The reconciliation "%(reconciliation)s" is delegated to the line "%(line)s".
You may need to cancel its move "%(move)s".</field>
</record>
<record model="ir.message" id="msg_reconciliation_line_not_valid">
<field name="text">You cannot reconcile non-valid line "%(line)s".</field>
</record>
@@ -167,5 +171,24 @@ Use the current period to cancel the move?</field>
<record model="ir.message" id="msg_line_party_set">
<field name="text">To create a line on account "%(account)s", you must remove the party on line "%(line)s".</field>
</record>
<record model="ir.message" id="msg_group_line">
<field name="text">Line "%(line)s" can not be grouped.</field>
</record>
<record model="ir.message" id="msg_group_line_single">
<field name="text">You cannot group a single line.</field>
</record>
<record model="ir.message" id="msg_group_line_many_parties">
<field name="text">You cannot group lines with different parties.</field>
</record>
<record model="ir.message" id="msg_group_line_same_company">
<field name="text">You cannot group lines of different companies.</field>
</record>
<record model="ir.message" id="msg_group_line_same_second_currency">
<field name="text">You cannot group lines of different second currencies.</field>
</record>
<record model="ir.message" id="msg_group_line_maximum_account">
<field name="text">You cannot group lines from more than 2 different accounts.</field>
</record>
</data>
</tryton>
+195 -3
View File
@@ -2,7 +2,7 @@
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
import datetime
from itertools import groupby, combinations
from itertools import groupby, combinations, chain
from operator import itemgetter
from collections import defaultdict
@@ -26,7 +26,7 @@ from trytond.tools import reduce_ids, grouped_slice
from trytond.config import config
from .exceptions import (PostError, MoveDatesError, CancelWarning,
ReconciliationError)
ReconciliationError, DeleteDelegatedWarning, GroupLineError)
__all__ = ['Move', 'Reconciliation', 'Line', 'WriteOff', 'OpenJournalAsk',
'OpenJournal', 'OpenAccount',
@@ -34,6 +34,7 @@ __all__ = ['Move', 'Reconciliation', 'Line', 'WriteOff', 'OpenJournalAsk',
'UnreconcileLines',
'Reconcile', 'ReconcileShow',
'CancelMoves', 'CancelMovesDefault',
'GroupLines', 'GroupLinesStart',
'PrintGeneralJournalStart', 'PrintGeneralJournal', 'GeneralJournal']
_MOVE_STATES = {
@@ -430,6 +431,9 @@ class Reconciliation(ModelSQL, ModelView):
'Lines')
date = fields.Date('Date', required=True, select=True,
help='Highest date of the reconciled lines')
delegate_to = fields.Many2One(
'account.move.line', "Delegate To", ondelete="RESTRICT", select=True,
help="The line to which the reconciliation status is delegated.")
@classmethod
def __register__(cls, module_name):
@@ -479,6 +483,21 @@ class Reconciliation(ModelSQL, ModelView):
super(Reconciliation, cls).validate(reconciliations)
cls.check_lines(reconciliations)
@classmethod
def delete(cls, reconciliations):
pool = Pool()
Warning = pool.get('res.user.warning')
for reconciliation in reconciliations:
if reconciliation.delegate_to:
key = '%s.delete.delegated' % reconciliation
if Warning.check(key):
raise DeleteDelegatedWarning(key,
gettext('account.msg_reconciliation_delete_delegated',
reconciliation=reconciliation.rec_name,
line=reconciliation.delegate_to.rec_name,
move=reconciliation.delegate_to.move.rec_name))
super().delete(reconciliations)
@classmethod
def check_lines(cls, reconciliations):
Lang = Pool().get('ir.lang')
@@ -631,6 +650,9 @@ class Line(ModelSQL, ModelView):
], 'State', readonly=True, required=True, select=True)
reconciliation = fields.Many2One('account.move.reconciliation',
'Reconciliation', readonly=True, ondelete='SET NULL', select=True)
reconciliations_delegated = fields.One2Many(
'account.move.reconciliation', 'delegate_to',
"Reconciliations Delegated", readonly=True)
tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines')
move_state = fields.Function(fields.Selection([
('draft', 'Draft'),
@@ -1158,13 +1180,15 @@ class Line(ModelSQL, ModelView):
@classmethod
def reconcile(
cls, *lines_list, date=None, writeoff=None, description=None):
cls, *lines_list, date=None, writeoff=None, description=None,
delegate_to=None):
"""
Reconcile each list of lines together.
The writeoff keys are: date, method and description.
"""
pool = Pool()
Reconciliation = pool.get('account.move.reconciliation')
delegate_to = delegate_to.id if delegate_to else None
reconciliations = []
for lines in lines_list:
@@ -1200,6 +1224,7 @@ class Line(ModelSQL, ModelView):
reconciliations.append({
'lines': [('add', [x.id for x in lines])],
'date': max(l.date for l in lines),
'delegate_to': delegate_to,
})
return Reconciliation.create(reconciliations)
@@ -1782,6 +1807,173 @@ class CancelMovesDefault(ModelView):
description = fields.Char('Description')
class GroupLines(Wizard):
"Group Lines"
__name__ = 'account.move.line.group'
start = StateView('account.move.line.group.start',
'account.move_line_group_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Group", 'group', 'tryton-ok', default=True),
])
group = StateTransition()
def transition_group(self):
pool = Pool()
Line = pool.get('account.move.line')
lines = Line.browse(Transaction().context['active_ids'])
grouping = self.grouping(lines)
move, balance_line = self.get_move(lines, grouping)
move.save()
to_reconcile = defaultdict(list)
for line in chain(lines, move.lines):
if line.account.reconcile:
to_reconcile[line.account].append(line)
balance_line.move = move
balance_line.save()
for lines in to_reconcile.values():
Line.reconcile(lines, delegate_to=balance_line)
return 'end'
@classmethod
def grouping(cls, lines):
if len(lines) == 1:
raise GroupLineError(gettext('account.msg_group_line_single'))
companies = set()
parties = set()
accounts = set()
second_currencies = set()
for line in lines:
if not cls.allow_grouping(line.account):
raise GroupLineError(gettext('account.msg_group_line'))
companies.add(line.move.company)
parties.add(line.party)
accounts.add(line.account)
second_currencies.add(line.second_currency)
try:
company, = companies
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_same_company'))
try:
party, = parties
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_many_parties'))
try:
second_currency, = second_currencies
except ValueError:
raise GroupLineError(
gettext('account.msg_group_line_same_second_currency'))
if len(accounts) > 2:
raise GroupLineError(
gettext('account.msg_group_line_maximum_account'))
return {
'company': company,
'party': party,
'second_currency': second_currency,
'accounts': accounts,
}
@classmethod
def allow_grouping(cls, account):
return account.type.payable or account.type.receivable
def get_move(self, lines, grouping, date=None):
pool = Pool()
Date = pool.get('ir.date')
Move = pool.get('account.move')
Period = pool.get('account.period')
Line = pool.get('account.move.line')
if not date:
date = Date.today()
period = Period.find(grouping['company'].id, date=date)
move = Move()
move.date = date
move.period = period
move.journal = self.start.journal
move.description = self.start.description
accounts = {a: 0 for a in grouping['accounts']}
amount_second_currency = 0
maturity_dates = {a: None for a in grouping['accounts']}
counterpart_lines = []
for line in lines:
if maturity_dates[line.account]:
maturity_dates[line.account] = min(
maturity_dates[line.account], line.maturity_date)
else:
maturity_dates[line.account] = line.maturity_date
line = self._counterpart_line(line)
accounts[line.account] += line.debit - line.credit
if line.amount_second_currency:
amount_second_currency += line.amount_second_currency
counterpart_lines.append(line)
move.lines = counterpart_lines
balance_line = None
if len(accounts) == 1:
account, = grouping['accounts']
amount = -accounts[account]
if amount:
balance_line = Line()
balance_line.account = account
else:
first, second = grouping['accounts']
if accounts[first] != accounts[second]:
balance_line = Line()
amount = -(accounts[first] + accounts[second])
if abs(accounts[first]) > abs(accounts[second]):
balance_line.account = first
else:
balance_line.account = second
if balance_line:
if balance_line.account.party_required:
balance_line.party = grouping['party']
if amount > 0:
balance_line.debit, balance_line.credit = amount, 0
else:
balance_line.debit, balance_line.credit = 0, -amount
if grouping['second_currency']:
balance_line.second_currency = grouping['second_currency']
balance_line.amount_second_currency = (
-amount_second_currency)
balance_line.maturity_date = maturity_dates[balance_line.account]
return move, balance_line
def _counterpart_line(self, line):
pool = Pool()
Line = pool.get('account.move.line')
counterpart = Line()
counterpart.account = line.account
counterpart.debit = line.credit
counterpart.credit = line.debit
counterpart.party = line.party
counterpart.second_currency = line.second_currency
if line.second_currency:
counterpart.amount_second_currency = -line.amount_second_currency
else:
counterpart.amount_second_currency = None
return counterpart
class GroupLinesStart(ModelView):
"Group Lines"
__name__ = 'account.move.line.group.start'
journal = fields.Many2One('account.journal', "Journal", required=True)
description = fields.Char("Description")
class PrintGeneralJournalStart(ModelView):
'Print General Journal'
__name__ = 'account.move.print_general_journal.start'
+16
View File
@@ -392,6 +392,22 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.action.wizard" id="act_group_lines_wizard">
<field name="name">Group Lines</field>
<field name="wiz_name">account.move.line.group</field>
</record>
<record model="ir.action.keyword" id="act_group_lines_keyword1">
<field name="keyword">form_action</field>
<field name="model">account.move.line,-1</field>
<field name="action" ref="act_group_lines_wizard"/>
</record>
<record model="ir.ui.view" id="move_line_group_start_view_form">
<field name="model">account.move.line.group.start</field>
<field name="type">form</field>
<field name="name">move_line_group_start_form</field>
</record>
<record model="ir.ui.view" id="print_general_journal_start_view_form">
<field name="model">account.move.print_general_journal.start</field>
<field name="type">form</field>
+131
View File
@@ -0,0 +1,131 @@
====================
Group Lines Scenario
====================
Imports::
>>> import datetime as dt
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.currency.tests.tools import get_currency
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> from trytond.modules.account.tests.tools import create_fiscalyear, \
... create_chart, get_accounts
Install account::
>>> config = activate_modules('account')
Create company::
>>> usd = get_currency('USD')
>>> eur = get_currency('EUR')
>>> _ = create_company(currency=usd)
>>> company = get_company()
Create fiscal year::
>>> fiscalyear = create_fiscalyear(company)
>>> fiscalyear.click('create_period')
>>> period = fiscalyear.periods[0]
Create chart of accounts::
>>> _ = create_chart(company)
>>> accounts = get_accounts(company)
>>> payable = accounts['payable']
>>> receivable = accounts['receivable']
>>> revenue = accounts['revenue']
>>> expense = accounts['expense']
Create parties::
>>> Party = Model.get('party.party')
>>> party = Party(name="Party")
>>> party.save()
Create Lines to group::
>>> Journal = Model.get('account.journal')
>>> Move = Model.get('account.move')
>>> journal_revenue, = Journal.find([
... ('code', '=', 'REV'),
... ])
>>> journal_expense, = Journal.find([
... ('code', '=', 'EXP'),
... ])
>>> move = Move()
>>> move.journal = journal_revenue
>>> move.date = period.start_date
>>> line = move.lines.new()
>>> line.account = revenue
>>> line.credit = Decimal(100)
>>> line = move.lines.new()
>>> line.account = receivable
>>> line.party = party
>>> line.debit = Decimal(100)
>>> line.second_currency = eur
>>> line.amount_second_currency = Decimal(90)
>>> line.maturity_date = period.start_date + dt.timedelta(days=2)
>>> move.save()
>>> move = Move()
>>> move.journal = journal_expense
>>> move.date = period.start_date
>>> line = move.lines.new()
>>> line.account = expense
>>> line.debit = Decimal(50)
>>> line = move.lines.new()
>>> line.account = payable
>>> line.party = party
>>> line.credit = Decimal(50)
>>> line.second_currency = eur
>>> line.amount_second_currency = Decimal(45)
>>> line.maturity_date = period.start_date + dt.timedelta(days=4)
>>> move.save()
>>> receivable.reload()
>>> receivable.balance, receivable.amount_second_currency
(Decimal('100.00'), Decimal('90.00'))
>>> payable.reload()
>>> payable.balance, payable.amount_second_currency
(Decimal('-50.00'), Decimal('-45.00'))
Group lines::
>>> journal_cash, = Journal.find([
... ('code', '=', 'CASH'),
... ])
>>> Line = Model.get('account.move.line')
>>> lines = Line.find([('account', 'in', [payable.id, receivable.id])])
>>> len(lines)
2
>>> group = Wizard('account.move.line.group', lines)
>>> group.form.journal = journal_cash
>>> group.form.description = "Group lines"
>>> group.execute('group')
>>> receivable.reload()
>>> receivable.balance, receivable.amount_second_currency
(Decimal('50.00'), Decimal('45.00'))
>>> payable.reload()
>>> payable.balance, payable.amount_second_currency
(Decimal('0.00'), Decimal('0.00'))
>>> Reconciliation = Model.get('account.move.reconciliation')
>>> reconciliations = Reconciliation.find([])
>>> len(reconciliations)
2
>>> all(r.delegate_to for r in reconciliations)
True
>>> delegate_to = reconciliations[0].delegate_to
>>> delegate_to.account == receivable
True
>>> delegate_to.debit
Decimal('50')
>>> delegate_to.maturity_date == period.start_date + dt.timedelta(days=2)
True
+5
View File
@@ -1321,6 +1321,11 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_move_line_group.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_move_template.rst',
tearDown=doctest_teardown, encoding='utf-8',
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="journal"/>
<field name="journal"/>
<newline/>
<label name="description"/>
<field name="description" colspan="3"/>
</form>
+6 -2
View File
@@ -1,8 +1,12 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form col="2">
<form>
<label name="name"/>
<field name="name"/>
<field name="lines" colspan="2"/>
<label name="date"/>
<field name="date"/>
<label name="delegate_to"/>
<field name="delegate_to"/>
<field name="lines" colspan="4"/>
</form>
+2
View File
@@ -3,4 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="name"/>
<field name="date"/>
<field name="delegate_to"/>
</tree>