diff --git a/CHANGELOG b/CHANGELOG
index 0b5968592e..2a4d65d444 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Add cancel to Account Move
+
Version 3.2.0 - 2014-04-21
* Bug fixes (see mercurial logs for details)
* Add start/end date to tax
diff --git a/__init__.py b/__init__.py
index 2be0f47951..73ffc5113a 100644
--- a/__init__.py
+++ b/__init__.py
@@ -89,6 +89,7 @@ def register():
ReconcileLines,
UnreconcileLines,
OpenReconcileLines,
+ CancelMoves,
PrintGeneralJournal,
OpenChartTaxCode,
OpenTaxCode,
diff --git a/move.py b/move.py
index 76b310ecb6..95dc9f6f38 100644
--- a/move.py
+++ b/move.py
@@ -4,6 +4,8 @@ from decimal import Decimal
import datetime
from itertools import groupby
from operator import itemgetter
+from collections import defaultdict
+
from sql.aggregate import Sum
from sql.conditionals import Coalesce
@@ -21,7 +23,8 @@ from trytond.tools import reduce_ids
__all__ = ['Move', 'Reconciliation', 'Line', 'OpenJournalAsk',
'OpenJournal', 'OpenAccount', 'ReconcileLinesWriteOff', 'ReconcileLines',
'UnreconcileLines', 'OpenReconcileLinesStart',
- 'OpenReconcileLines', 'FiscalYearLine', 'FiscalYear2',
+ 'OpenReconcileLines', 'CancelMoves',
+ 'FiscalYearLine', 'FiscalYear2',
'PrintGeneralJournalStart', 'PrintGeneralJournal', 'GeneralJournal']
__metaclass__ = PoolMeta
@@ -86,6 +89,9 @@ class Move(ModelSQL, ModelView):
'because it\'s date is outside its period.'),
'draft_closed_period': ('You can not set to draft move '
'"%(move)s" because period "%(period)s" is closed.'),
+ 'period_cancel': (
+ 'The period of move "%s" is closed.\n'
+ 'Use the current period?'),
})
cls._buttons.update({
'post': {
@@ -156,7 +162,7 @@ class Move(ModelSQL, ModelView):
@classmethod
def _get_origin(cls):
'Return list of Model names for origin Reference'
- return ['account.fiscalyear']
+ return ['account.fiscalyear', 'account.move']
@classmethod
def get_origin(cls):
@@ -334,6 +340,50 @@ class Move(ModelSQL, ModelView):
values=[state],
where=red_sql))
+ def _cancel_default(self):
+ 'Return default dictionary to cancel move'
+ pool = Pool()
+ Date = pool.get('ir.date')
+ Period = pool.get('account.period')
+
+ default = {
+ 'origin': str(self),
+ }
+ if self.period.state == 'close':
+ self.raise_user_warning('%s.cancel' % self,
+ 'period_cancel', self.rec_name)
+ date = Date.today()
+ period_id = Period.find(self.company.id, date=date)
+ default.update({
+ 'date': date,
+ 'period': period_id,
+ })
+ return default
+
+ def cancel(self):
+ 'Return a cancel move'
+ pool = Pool()
+ Line = pool.get('account.move.line')
+ TaxLine = pool.get('account.tax.line')
+ default = self._cancel_default()
+ cancel_move, = Move.copy([self], default=default)
+ lines = []
+ tax_lines = []
+ for line in cancel_move.lines:
+ line.debit *= -1
+ line.credit *= -1
+ lines.extend(([line], line._save_values))
+ line._values = None
+ for tax_line in line.tax_lines:
+ tax_line.amount *= -1
+ tax_lines.extend(([tax_line], tax_line._save_values))
+ tax_line._values = None
+ if lines:
+ Line.write(*lines)
+ if tax_lines:
+ TaxLine.write(*tax_lines)
+ return cancel_move
+
@classmethod
@ModelView.button
def post(cls, moves):
@@ -1670,6 +1720,29 @@ class OpenReconcileLines(Wizard):
return action, {}
+class CancelMoves(Wizard):
+ 'Cancel Moves'
+ __name__ = 'account.move.cancel'
+ start_state = 'cancel'
+ cancel = StateTransition()
+
+ def transition_cancel(self):
+ pool = Pool()
+ Move = pool.get('account.move')
+ Line = pool.get('account.move.line')
+
+ moves = Move.browse(Transaction().context['active_ids'])
+ for move in moves:
+ cancel_move = move.cancel()
+ to_reconcile = defaultdict(list)
+ for line in move.lines + cancel_move.lines:
+ if line.account.reconcile:
+ to_reconcile[line.account].append(line)
+ for lines in to_reconcile.itervalues():
+ Line.reconcile(lines)
+ return 'end'
+
+
class FiscalYearLine(ModelSQL):
'Fiscal Year - Move Line'
__name__ = 'account.fiscalyear-account.move.line'
diff --git a/move.xml b/move.xml
index 68ea2f9f95..9292efe47f 100644
--- a/move.xml
+++ b/move.xml
@@ -294,6 +294,16 @@ this repository contains the full copyright notices and license terms. -->
action="act_open_reconcile_lines"
id="menu_open_reconcile_lines"/>
+
+ Cancel Moves
+ account.move.cancel
+
+
+ form_action
+ account.move,-1
+
+
+
account.move.print_general_journal.start
form
diff --git a/tests/scenario_move_cancel.rst b/tests/scenario_move_cancel.rst
new file mode 100644
index 0000000000..140d3c0197
--- /dev/null
+++ b/tests/scenario_move_cancel.rst
@@ -0,0 +1,158 @@
+====================
+Move Cancel Scenario
+====================
+
+=============
+General Setup
+=============
+
+Imports::
+
+ >>> import datetime
+ >>> from dateutil.relativedelta import relativedelta
+ >>> from decimal import Decimal
+ >>> from proteus import config, Model, Wizard
+ >>> today = datetime.date.today()
+
+Create database::
+
+ >>> config = config.set_trytond()
+ >>> config.pool.test = True
+
+Install account::
+
+ >>> Module = Model.get('ir.module.module')
+ >>> modules = Module.find([
+ ... ('name', '=', 'account'),
+ ... ])
+ >>> Module.install([x.id for x in modules], config.context)
+ >>> Wizard('ir.module.module.install_upgrade').execute('upgrade')
+
+Create company::
+
+ >>> Currency = Model.get('currency.currency')
+ >>> CurrencyRate = Model.get('currency.currency.rate')
+ >>> Company = Model.get('company.company')
+ >>> Party = Model.get('party.party')
+ >>> company_config = Wizard('company.company.config')
+ >>> company_config.execute('company')
+ >>> company = company_config.form
+ >>> party = Party(name='Dunder Mifflin')
+ >>> party.save()
+ >>> company.party = party
+ >>> currencies = Currency.find([('code', '=', 'USD')])
+ >>> if not currencies:
+ ... currency = Currency(name='U.S. Dollar', symbol='$', code='USD',
+ ... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]',
+ ... mon_decimal_point='.', mon_thousands_sep=',')
+ ... currency.save()
+ ... CurrencyRate(date=today + relativedelta(month=1, day=1),
+ ... rate=Decimal('1.0'), currency=currency).save()
+ ... else:
+ ... currency, = currencies
+ >>> company.currency = currency
+ >>> company_config.execute('add')
+ >>> company, = Company.find()
+
+Reload the context::
+
+ >>> User = Model.get('res.user')
+ >>> config._context = User.get_preferences(True, config.context)
+
+Create fiscal year::
+
+ >>> FiscalYear = Model.get('account.fiscalyear')
+ >>> Sequence = Model.get('ir.sequence')
+ >>> SequenceStrict = Model.get('ir.sequence.strict')
+ >>> fiscalyear = FiscalYear(name='%s' % today.year)
+ >>> fiscalyear.start_date = today + relativedelta(month=1, day=1)
+ >>> fiscalyear.end_date = today + relativedelta(month=12, day=31)
+ >>> fiscalyear.company = company
+ >>> post_move_sequence = Sequence(name='%s' % today.year,
+ ... code='account.move', company=company)
+ >>> post_move_sequence.save()
+ >>> fiscalyear.post_move_sequence = post_move_sequence
+ >>> fiscalyear.save()
+ >>> FiscalYear.create_period([fiscalyear.id], config.context)
+ >>> period = fiscalyear.periods[0]
+
+Create chart of accounts::
+
+ >>> AccountTemplate = Model.get('account.account.template')
+ >>> Account = Model.get('account.account')
+ >>> account_template, = AccountTemplate.find([('parent', '=', None)])
+ >>> create_chart = Wizard('account.create_chart')
+ >>> create_chart.execute('account')
+ >>> create_chart.form.account_template = account_template
+ >>> create_chart.form.company = company
+ >>> create_chart.execute('create_account')
+ >>> receivable, = Account.find([
+ ... ('kind', '=', 'receivable'),
+ ... ('company', '=', company.id),
+ ... ])
+ >>> payable, = Account.find([
+ ... ('kind', '=', 'payable'),
+ ... ('company', '=', company.id),
+ ... ])
+ >>> revenue, = Account.find([
+ ... ('kind', '=', 'revenue'),
+ ... ('company', '=', company.id),
+ ... ])
+ >>> create_chart.form.account_receivable = receivable
+ >>> create_chart.form.account_payable = payable
+ >>> create_chart.execute('create_properties')
+
+Create parties::
+
+ >>> Party = Model.get('party.party')
+ >>> customer = Party(name='Customer')
+ >>> customer.save()
+
+Create Move to cancel::
+
+ >>> Journal = Model.get('account.journal')
+ >>> Move = Model.get('account.move')
+ >>> journal_revenue, = Journal.find([
+ ... ('code', '=', 'REV'),
+ ... ])
+ >>> journal_cash, = Journal.find([
+ ... ('code', '=', 'CASH'),
+ ... ])
+ >>> move = Move()
+ >>> move.period = period
+ >>> move.journal = journal_revenue
+ >>> move.date = period.start_date
+ >>> line = move.lines.new()
+ >>> line.account = revenue
+ >>> line.credit = Decimal(42)
+ >>> line = move.lines.new()
+ >>> line.account = receivable
+ >>> line.debit = Decimal(42)
+ >>> line.party = customer
+ >>> move.save()
+ >>> revenue.reload()
+ >>> revenue.credit
+ Decimal('42.00')
+ >>> receivable.reload()
+ >>> receivable.debit
+ Decimal('42.00')
+
+Cancel Move::
+
+ >>> cancel_move = Wizard('account.move.cancel', [move])
+ >>> cancel_move.state
+ 'end'
+ >>> move.reload()
+ >>> line, = [l for l in move.lines if l.account == receivable]
+ >>> bool(line.reconciliation)
+ True
+ >>> cancel_move, = [l.move for l in line.reconciliation.lines
+ ... if l.move != move]
+ >>> cancel_move.origin == move
+ True
+ >>> revenue.reload()
+ >>> revenue.credit
+ Decimal('0.00')
+ >>> receivable.reload()
+ >>> receivable.debit
+ Decimal('0.00')
diff --git a/tests/test_account.py b/tests/test_account.py
index b7a9b9eae8..1fed67a01a 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -436,4 +436,8 @@ def suite():
'scenario_account_reconciliation.rst',
setUp=doctest_dropdb, tearDown=doctest_dropdb, encoding='utf-8',
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
+ suite.addTests(doctest.DocFileSuite(
+ 'scenario_move_cancel.rst',
+ setUp=doctest_dropdb, tearDown=doctest_dropdb, encoding='utf-8',
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite