diff --git a/modules/account/CHANGELOG b/modules/account/CHANGELOG
index 6ac00b8b4e..5b8c7a5e44 100644
--- a/modules/account/CHANGELOG
+++ b/modules/account/CHANGELOG
@@ -1,3 +1,4 @@
+* Allow copying move from closed period
* Replace Accounting Party by Accounting group
Version 7.2.0 - 2024-04-29
diff --git a/modules/account/exceptions.py b/modules/account/exceptions.py
index b87358d1dd..a491d34dd7 100644
--- a/modules/account/exceptions.py
+++ b/modules/account/exceptions.py
@@ -81,6 +81,10 @@ class MoveTemplateKeywordValidationError(ValidationError):
pass
+class CopyWarning(UserWarning):
+ pass
+
+
class CancelWarning(UserWarning):
pass
diff --git a/modules/account/message.xml b/modules/account/message.xml
index 7d7f5c5205..4c1d162443 100644
--- a/modules/account/message.xml
+++ b/modules/account/message.xml
@@ -134,6 +134,10 @@ this repository contains the full copyright notices and license terms. -->
You cannot modify posted move "%(move)s".
+
+ The period of move "%(move)s" is closed.
+Use the current period to copy the move?
+
The period of move "%(move)s" is closed.
Use the current period to cancel the move?
diff --git a/modules/account/move.py b/modules/account/move.py
index 0d7edbb6a5..0453b190c8 100644
--- a/modules/account/move.py
+++ b/modules/account/move.py
@@ -27,9 +27,10 @@ from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
from .exceptions import (
- AccountMissing, CancelDelegatedWarning, CancelWarning, DelegateLineError,
- GroupLineError, JournalMissing, PeriodNotFoundError, PostError,
- ReconciliationDeleteWarning, ReconciliationError, RescheduleLineError)
+ AccountMissing, CancelDelegatedWarning, CancelWarning, CopyWarning,
+ DelegateLineError, GroupLineError, JournalMissing, PeriodNotFoundError,
+ PostError, ReconciliationDeleteWarning, ReconciliationError,
+ RescheduleLineError)
_MOVE_STATES = {
'readonly': Eval('state') == 'posted',
@@ -335,14 +336,51 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
@classmethod
def copy(cls, moves, default=None):
+ pool = Pool()
+ Date = pool.get('ir.date')
+ Period = pool.get('account.period')
+ Warning = pool.get('res.user.warning')
+
if default is None:
default = {}
else:
default = default.copy()
+
+ def _check_period(origin):
+ period = Period(origin['period'])
+ if period.state == 'closed':
+ move = cls(origin['id'])
+ key = Warning.format('copy', [move])
+ if Warning.check(key):
+ raise CopyWarning(key,
+ gettext('account.msg_move_copy_closed_period',
+ move=move))
+ return False
+ else:
+ return True
+
+ def default_period(origin):
+ if not _check_period(origin):
+ with Transaction().set_context(company=origin['company']):
+ today = Date.today()
+ period = Period.find(origin['company'], date=today)
+ return period.id
+ else:
+ return origin['period']
+
+ def default_date(origin):
+ if not _check_period(origin):
+ with Transaction().set_context(company=origin['company']):
+ return Date.today()
+ else:
+ return origin['date']
+
default.setdefault('number', None)
default.setdefault('post_number', None)
default.setdefault('state', cls.default_state())
default.setdefault('post_date', None)
+ default.setdefault('period', default_period)
+ default.setdefault('date', default_date)
return super().copy(moves, default=default)
@classmethod
diff --git a/modules/account/tests/scenario_move_copy.rst b/modules/account/tests/scenario_move_copy.rst
new file mode 100644
index 0000000000..2cd82595b0
--- /dev/null
+++ b/modules/account/tests/scenario_move_copy.rst
@@ -0,0 +1,95 @@
+==================
+Move Copy Scenario
+==================
+
+Imports::
+
+ >>> from decimal import Decimal
+
+ >>> from proteus import Model
+ >>> from trytond.modules.account.tests.tools import (
+ ... create_chart, create_fiscalyear, get_accounts)
+ >>> from trytond.modules.company.tests.tools import create_company
+ >>> from trytond.tests.tools import (
+ ... activate_modules, assertEqual, assertNotEqual, assertTrue)
+
+Activate modules::
+
+ >>> config = activate_modules('account')
+
+ >>> Journal = Model.get('account.journal')
+ >>> Move = Model.get('account.move')
+ >>> Party = Model.get('party.party')
+
+Create a company::
+
+ >>> _ = create_company()
+
+Create a fiscal year::
+
+ >>> fiscalyear = create_fiscalyear()
+ >>> fiscalyear.click('create_period')
+ >>> period = fiscalyear.periods[0]
+
+Create a chart of accounts::
+
+ >>> _ = create_chart()
+ >>> accounts = get_accounts()
+
+Create a party::
+
+ >>> party = Party(name="Party")
+ >>> party.save()
+
+Create a posted move::
+
+ >>> move = Move()
+ >>> move.period = period
+ >>> move.journal, = Journal.find([('code', '=', 'REV')])
+ >>> move.date = period.start_date
+ >>> line = move.lines.new()
+ >>> line.account = accounts['revenue']
+ >>> line.credit = Decimal(42)
+ >>> line = move.lines.new()
+ >>> line.account = accounts['receivable']
+ >>> line.debit = Decimal(42)
+ >>> line.party = party
+ >>> move.save()
+ >>> move.click('post')
+ >>> move.state
+ 'posted'
+
+Copy move on open period::
+
+ >>> copy_move, = move.duplicate()
+ >>> assertTrue(copy_move.number)
+ >>> copy_move.post_number
+ >>> copy_move.state
+ 'draft'
+ >>> copy_move.post_date
+ >>> assertEqual(copy_move.period, move.period)
+ >>> assertEqual(copy_move.date, move.date)
+ >>> copy_move.delete()
+
+Copy move on closed period::
+
+ >>> period.click('close')
+ >>> period.state
+ 'closed'
+
+ >>> copy_move, = move.duplicate()
+ Traceback (most recent call last):
+ ...
+ CopyWarning: ...
+
+ >>> config.skip_warning = True
+ >>> copy_move, = move.duplicate()
+ >>> assertTrue(copy_move.number)
+ >>> copy_move.post_number
+ >>> copy_move.state
+ 'draft'
+ >>> copy_move.post_date
+ >>> assertNotEqual(copy_move.period, move.period)
+ >>> copy_move.period.state
+ 'open'
+ >>> assertNotEqual(copy_move.date, move.date)