mirror of
https://github.com/OCA/account-financial-tools.git
synced 2025-02-02 12:47:26 +02:00
[MIG] account_move_batch_validate: Migration to 10.0 (#510)
This commit is contained in:
committed by
Iryna Vushnevska
parent
10dab41896
commit
7a19f5b33d
@@ -5,20 +5,14 @@ Account Move Batch Validate
|
||||
===========================
|
||||
|
||||
This module provides a wizard to post many Journal Entries in batch. it
|
||||
uses the queue system introduced by the OpenERP Connector to handle a
|
||||
uses the queue system introduced by the Odoo Queue job module to handle a
|
||||
big quantity of moves in batch.
|
||||
|
||||
The module account_default_draft_move introduces a workflow where the
|
||||
Journal Entries are always entered in OpenERP in draft state, and the
|
||||
posting happens later, for example at the end of the period. The core
|
||||
account module provides a wizard to post all the moves in the period,
|
||||
but that is problematic when there are many moves.
|
||||
|
||||
The posting of a move takes some time, and doing that synchronously,
|
||||
in one transaction is problematic.
|
||||
|
||||
In this module, we leverage the power of the queue system of the
|
||||
OpenERP Connector, that can be very well used without other concepts
|
||||
Odoo queue job module, that can be very well used without other concepts
|
||||
like Backends and Bindings.
|
||||
|
||||
This approach provides many advantages, similar to the ones we get
|
||||
@@ -56,6 +50,7 @@ Contributors
|
||||
* Rudolf Schnapka <rs@techno-flex.de>
|
||||
* Stéphane Bidoul (ACSONE) <stephane.bidoul@acsone.eu>
|
||||
* Adrien Peiffer (ACSONE) <adrien.peiffer@acsone.eu>
|
||||
* Benjamin Willig (ACSONE) <benjamin.willig@acsone.eu>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
# #
|
||||
# Author: Leonardo Pistone
|
||||
# Copyright 2014 Camptocamp SA
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU Affero General Public License as #
|
||||
# published by the Free Software Foundation, either version 3 of the #
|
||||
# License, or (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU Affero General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Affero General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
###############################################################################
|
||||
"""Account Move Batch Validate."""
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account # noqa
|
||||
from . import wizard # noqa
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -1,45 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
# #
|
||||
# Author: Leonardo Pistone
|
||||
# Copyright 2014 Camptocamp SA
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU Affero General Public License as #
|
||||
# published by the Free Software Foundation, either version 3 of the #
|
||||
# License, or (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU Affero General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Affero General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
###############################################################################
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
'name': "Account Move Batch Validate",
|
||||
'version': '8.0.0.2.0',
|
||||
'author': "Camptocamp,Odoo Community Association (OCA)",
|
||||
'version': '10.0.1.0.0',
|
||||
'author': "Camptocamp, Odoo Community Association (OCA)",
|
||||
'website': 'http://www.camptocamp.com',
|
||||
'category': 'Finance',
|
||||
'complexity': 'normal',
|
||||
'depends': [
|
||||
'account',
|
||||
'account_default_draft_move',
|
||||
'connector',
|
||||
'queue_job',
|
||||
],
|
||||
'website': 'http://www.camptocamp.com',
|
||||
'data': [
|
||||
'account_view.xml',
|
||||
'wizard/move_marker_view.xml',
|
||||
'views/account_move.xml',
|
||||
'wizard/account_move_validate.xml',
|
||||
],
|
||||
'test': [
|
||||
'test/batch_validate.yml',
|
||||
'test/batch_validate_then_unmark.yml',
|
||||
'test/batch_validate_then_delete_move.yml',
|
||||
],
|
||||
'installable': False,
|
||||
'images': [],
|
||||
'installable': True,
|
||||
'license': 'AGPL-3',
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
# #
|
||||
# Author: Leonardo Pistone
|
||||
# Copyright 2014 Camptocamp SA
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU Affero General Public License as #
|
||||
# published by the Free Software Foundation, either version 3 of the #
|
||||
# License, or (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU Affero General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Affero General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
###############################################################################
|
||||
"""Accounting customisation for delayed posting."""
|
||||
|
||||
import logging
|
||||
|
||||
from openerp.osv import fields, orm
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from openerp.addons.connector.queue.job import job
|
||||
from openerp.addons.connector.session import ConnectorSession
|
||||
from openerp.addons.connector.queue.job import OpenERPJobStorage
|
||||
except ImportError:
|
||||
_logger.debug('Can not `import connector`.')
|
||||
import functools
|
||||
|
||||
def empty_decorator_factory(*argv, **kwargs):
|
||||
return functools.partial
|
||||
job = empty_decorator_factory
|
||||
|
||||
# do a massive write on account moves BLOCK_SIZE at a time
|
||||
BLOCK_SIZE = 1000
|
||||
|
||||
|
||||
class account_move(orm.Model):
|
||||
|
||||
"""We modify the account move to allow delayed posting."""
|
||||
|
||||
_name = 'account.move'
|
||||
_inherit = 'account.move'
|
||||
|
||||
_columns = {
|
||||
'to_post': fields.boolean(
|
||||
'Posting Requested',
|
||||
readonly=True,
|
||||
help='Check this box to mark the move for batch posting'
|
||||
),
|
||||
'post_job_uuid': fields.char(
|
||||
'UUID of the Job to approve this move'
|
||||
),
|
||||
}
|
||||
|
||||
def _delay_post_marked(self, cr, uid, eta=None, context=None):
|
||||
"""Create a job for every move marked for posting.
|
||||
|
||||
If some moves already have a job, they are skipped.
|
||||
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
|
||||
move_ids = self.search(cr, uid, [
|
||||
('to_post', '=', True),
|
||||
('post_job_uuid', '=', False),
|
||||
('state', '=', 'draft'),
|
||||
], context=context)
|
||||
name = self._name
|
||||
|
||||
# maybe not creating too many dictionaries will make us a bit faster
|
||||
values = {'post_job_uuid': None}
|
||||
_logger.info(
|
||||
u'{0} jobs for posting moves have been created.'.format(
|
||||
len(move_ids)
|
||||
)
|
||||
)
|
||||
|
||||
for move_id in move_ids:
|
||||
job_uuid = validate_one_move.delay(session, name, move_id,
|
||||
eta=eta)
|
||||
values['post_job_uuid'] = job_uuid
|
||||
self.write(cr, uid, [move_id], values)
|
||||
cr.commit()
|
||||
|
||||
def _cancel_jobs(self, cr, uid, context=None):
|
||||
"""Find moves where the mark has been removed and cancel the jobs.
|
||||
|
||||
For the moves that are posted already it's too late: we skip them.
|
||||
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
storage = OpenERPJobStorage(session)
|
||||
|
||||
move_ids = self.search(cr, uid, [
|
||||
('to_post', '=', False),
|
||||
('post_job_uuid', '!=', False),
|
||||
('state', '=', 'draft'),
|
||||
], context=context)
|
||||
|
||||
for move in self.browse(cr, uid, move_ids, context=context):
|
||||
job_rec = storage.load(move.post_job_uuid)
|
||||
if job_rec.state in (u'pending', u'enqueued'):
|
||||
job_rec.set_done(result=_(
|
||||
u'Task set to Done because the user unmarked the move'
|
||||
))
|
||||
storage.store(job_rec)
|
||||
|
||||
def mark_for_posting(self, cr, uid, move_ids, eta=None, context=None):
|
||||
"""Mark a list of moves for delayed posting, and enqueue the jobs."""
|
||||
if context is None:
|
||||
context = {}
|
||||
# For massive amounts of moves, this becomes necessary to avoid
|
||||
# MemoryError's
|
||||
|
||||
_logger.info(
|
||||
u'{0} moves marked for posting.'.format(len(move_ids))
|
||||
)
|
||||
|
||||
for start in xrange(0, len(move_ids), BLOCK_SIZE):
|
||||
self.write(
|
||||
cr,
|
||||
uid,
|
||||
move_ids[start:start + BLOCK_SIZE],
|
||||
{'to_post': True},
|
||||
context=context)
|
||||
# users like to see the flag sooner rather than later
|
||||
cr.commit()
|
||||
self._delay_post_marked(cr, uid, eta=eta, context=context)
|
||||
|
||||
def unmark_for_posting(self, cr, uid, move_ids, context=None):
|
||||
"""Unmark moves for delayed posting, and cancel the jobs."""
|
||||
if context is None:
|
||||
context = {}
|
||||
self.write(cr, uid, move_ids, {'to_post': False}, context=context)
|
||||
self._cancel_jobs(cr, uid, context=context)
|
||||
|
||||
|
||||
@job(default_channel='root.account_move_batch_validate')
|
||||
def validate_one_move(session, model_name, move_id):
|
||||
"""Validate a move, and leave the job reference in place."""
|
||||
move_pool = session.pool['account.move']
|
||||
if move_pool.exists(session.cr, session.uid, [move_id]):
|
||||
move_pool.button_validate(
|
||||
session.cr,
|
||||
session.uid,
|
||||
[move_id]
|
||||
)
|
||||
else:
|
||||
return _(u'Nothing to do because the record has been deleted')
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_move_to_post_tree" model="ir.ui.view">
|
||||
<field name="name">view.move.to_post.tree</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="to_check" position="after">
|
||||
<field name="to_post"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_to_post_form" model="ir.ui.view">
|
||||
<field name="name">view.move.to_post.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="to_check" position="after">
|
||||
<field name="to_post"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
5
account_move_batch_validate/models/__init__.py
Normal file
5
account_move_batch_validate/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_move
|
||||
101
account_move_batch_validate/models/account_move.py
Normal file
101
account_move_batch_validate/models/account_move.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.queue_job.job import job, Job
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
BLOCK_SIZE = 1000
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
|
||||
_inherit = 'account.move'
|
||||
|
||||
to_post = fields.Boolean(
|
||||
string="Posting requested", readonly=True,
|
||||
help="Check this box to mark the move for batch posting")
|
||||
post_job_uuid = fields.Char(string="UUID of the Job to approve this move")
|
||||
|
||||
@job(default_channel='root.account_move_batch_validate')
|
||||
def validate_one_move(self, move_id):
|
||||
move = self.browse(move_id)
|
||||
if move.exists():
|
||||
move.post()
|
||||
else:
|
||||
return _(u"Nothing to do because the record has been deleted")
|
||||
|
||||
@api.model
|
||||
def _delay_post_marked(self, eta=None):
|
||||
"""
|
||||
Create a job for every move marked for posting.
|
||||
If some moves already have a job, they are skipped.
|
||||
"""
|
||||
AccountMoveObj = self.env[self._name]
|
||||
|
||||
moves = self.search([
|
||||
('to_post', '=', True),
|
||||
('post_job_uuid', '=', False),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
|
||||
# maybe not creating too many dictionaries will make us a bit faster
|
||||
values = {'post_job_uuid': None}
|
||||
_logger.info(
|
||||
"%s jobs for posting moves have been created.", len(moves))
|
||||
|
||||
for move in moves:
|
||||
new_job = AccountMoveObj.with_delay(eta=eta).validate_one_move(
|
||||
move.id)
|
||||
values['post_job_uuid'] = new_job.uuid
|
||||
move.write(values)
|
||||
self.env.cr.commit() # pylint:disable=invalid-commit
|
||||
|
||||
@api.model
|
||||
def _cancel_post_jobs(self):
|
||||
"""
|
||||
Find moves where the mark has been removed and cancel the jobs.
|
||||
For the moves that are posted already it's too late: we skip them.
|
||||
"""
|
||||
moves = self.search([
|
||||
('to_post', '=', False),
|
||||
('post_job_uuid', '!=', False),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
|
||||
for move in moves:
|
||||
job_rec = Job.load(self.env, move.post_job_uuid)
|
||||
if job_rec.state in ('pending', 'enqueued'):
|
||||
job_rec.set_done(
|
||||
result=_("Task set to Done because the "
|
||||
"user unmarked the move."))
|
||||
job_rec.store()
|
||||
|
||||
@api.multi
|
||||
def mark_for_posting(self, eta=None):
|
||||
"""
|
||||
Mark a list of moves for delayed posting, and enqueue the jobs.
|
||||
For massive amounts of moves, this becomes necessary to avoid
|
||||
MemoryError's
|
||||
"""
|
||||
moves_count = len(self)
|
||||
_logger.info("%s moves marked for posting.", moves_count)
|
||||
|
||||
values = {'to_post': True}
|
||||
|
||||
for index in xrange(0, moves_count, BLOCK_SIZE):
|
||||
moves = self[index:index + BLOCK_SIZE]
|
||||
moves.write(values)
|
||||
# users like to see the flag sooner rather than later
|
||||
self.env.cr.commit() # pylint:disable=invalid-commit
|
||||
self._delay_post_marked(eta=eta)
|
||||
|
||||
@api.multi
|
||||
def unmark_for_posting(self):
|
||||
self.write({'to_post': False})
|
||||
self._cancel_post_jobs()
|
||||
@@ -1,61 +0,0 @@
|
||||
-
|
||||
I create a move
|
||||
-
|
||||
!record {model: account.move, id: move1}:
|
||||
journal_id: account.sales_journal
|
||||
line_id:
|
||||
- name: Receivable line
|
||||
account_id: account.a_recv
|
||||
debit: 1000.0
|
||||
- name: Sales line
|
||||
account_id: account.a_sale
|
||||
credit: 1000.0
|
||||
-
|
||||
I check that the move is still draft
|
||||
-
|
||||
!assert {model: account.move, id: move1}:
|
||||
- state == 'draft'
|
||||
-
|
||||
I create a wizard
|
||||
-
|
||||
!record {model: validate.account.move, id: wiz_marker1}:
|
||||
action: mark
|
||||
-
|
||||
I set the period and the journal on the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
move = self.pool['account.move'].browse(cr, uid, ref('move1'),
|
||||
context=context)
|
||||
journal_ids = [(6, 0, [move.journal_id.id])]
|
||||
period_ids = [(6, 0, [move.period_id.id])]
|
||||
vals = {'journal_ids': journal_ids,
|
||||
'period_ids': period_ids,
|
||||
}
|
||||
self.write(cr, uid, ref('wiz_marker1'), vals, context=context)
|
||||
-
|
||||
I run the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
context['automated_test_execute_now'] = True
|
||||
self.validate_move(
|
||||
cr, uid, [ref('wiz_marker1')], context=context
|
||||
)
|
||||
-
|
||||
I read the UUID from the move, I dequeue the job and run it
|
||||
-
|
||||
!python {model: account.move}: |
|
||||
from openerp.addons.connector.queue.job import OpenERPJobStorage
|
||||
from openerp.addons.connector.session import ConnectorSession
|
||||
|
||||
move = self.browse(cr, uid, ref('move1'), context=context)
|
||||
uuid = move.post_job_uuid
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
storage = OpenERPJobStorage(session)
|
||||
|
||||
myjob = storage.load(uuid)
|
||||
myjob.perform(session)
|
||||
-
|
||||
I check that the move is now approved
|
||||
-
|
||||
!assert {model: account.move, id: move1}:
|
||||
- state == 'posted'
|
||||
@@ -1,64 +0,0 @@
|
||||
-
|
||||
I create a move
|
||||
-
|
||||
!record {model: account.move, id: move3}:
|
||||
journal_id: account.sales_journal
|
||||
line_id:
|
||||
- name: Receivable line
|
||||
account_id: account.a_recv
|
||||
debit: 3000.0
|
||||
- name: Sales line
|
||||
account_id: account.a_sale
|
||||
credit: 3000.0
|
||||
-
|
||||
I check that the move is still draft
|
||||
-
|
||||
!assert {model: account.move, id: move3}:
|
||||
- state == 'draft'
|
||||
-
|
||||
I create a wizard with a long ETA
|
||||
-
|
||||
!record {model: validate.account.move, id: wiz_marker4}:
|
||||
action: mark
|
||||
eta: 10000
|
||||
-
|
||||
I set the period and the journal on the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
move = self.pool['account.move'].browse(cr, uid, ref('move3'),
|
||||
context=context)
|
||||
journal_ids = [(6, 0, [move.journal_id.id])]
|
||||
period_ids = [(6, 0, [move.period_id.id])]
|
||||
vals = {'journal_ids': journal_ids,
|
||||
'period_ids': period_ids,
|
||||
}
|
||||
self.write(cr, uid, ref('wiz_marker4'), vals, context=context)
|
||||
-
|
||||
I run the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
context['automated_test_execute_now'] = True
|
||||
self.validate_move(
|
||||
cr, uid, [ref('wiz_marker4')], context=context
|
||||
)
|
||||
-
|
||||
I read the UUID from the move, delete the move, then dequeue the job and run it.
|
||||
It should raise no exceptions.
|
||||
-
|
||||
!python {model: account.move}: |
|
||||
from openerp.addons.connector.queue.job import OpenERPJobStorage
|
||||
from openerp.addons.connector.session import ConnectorSession
|
||||
|
||||
move = self.browse(cr, uid, ref('move3'), context=context)
|
||||
uuid = move.post_job_uuid
|
||||
|
||||
assert uuid, 'The Job has not been created.'
|
||||
self.unlink(cr, uid, ref('move3'), context=context)
|
||||
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
storage = OpenERPJobStorage(session)
|
||||
|
||||
myjob = storage.load(uuid)
|
||||
myjob.perform(session)
|
||||
|
||||
assert myjob.result == u'Nothing to do because the record has been deleted'
|
||||
@@ -1,87 +0,0 @@
|
||||
-
|
||||
I create a move
|
||||
-
|
||||
!record {model: account.move, id: move2}:
|
||||
journal_id: account.sales_journal
|
||||
line_id:
|
||||
- name: Receivable line
|
||||
account_id: account.a_recv
|
||||
debit: 2000.0
|
||||
- name: Sales line
|
||||
account_id: account.a_sale
|
||||
credit: 2000.0
|
||||
-
|
||||
I check that the move is still draft
|
||||
-
|
||||
!assert {model: account.move, id: move2}:
|
||||
- state == 'draft'
|
||||
-
|
||||
I create a wizard with a long ETA
|
||||
-
|
||||
!record {model: validate.account.move, id: wiz_marker2}:
|
||||
action: mark
|
||||
eta: 10000
|
||||
-
|
||||
I set the period and the journal on the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
move = self.pool['account.move'].browse(cr, uid, ref('move2'),
|
||||
context=context)
|
||||
journal_ids = [(6, 0, [move.journal_id.id])]
|
||||
period_ids = [(6, 0, [move.period_id.id])]
|
||||
vals = {'journal_ids': journal_ids,
|
||||
'period_ids': period_ids,
|
||||
}
|
||||
self.write(cr, uid, ref('wiz_marker2'), vals, context=context)
|
||||
-
|
||||
I run the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
context['automated_test_execute_now'] = True
|
||||
self.validate_move(
|
||||
cr, uid, [ref('wiz_marker2')], context=context
|
||||
)
|
||||
-
|
||||
Now I change my mind and I create a wizard to unmark the moves
|
||||
-
|
||||
!record {model: validate.account.move, id: wiz_unmarker3}:
|
||||
action: unmark
|
||||
-
|
||||
I set the period and the journal on the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
move = self.pool['account.move'].browse(cr, uid, ref('move2'),
|
||||
context=context)
|
||||
journal_ids = [(6, 0, [move.journal_id.id])]
|
||||
period_ids = [(6, 0, [move.period_id.id])]
|
||||
vals = {'journal_ids': journal_ids,
|
||||
'period_ids': period_ids,
|
||||
}
|
||||
self.write(cr, uid, ref('wiz_unmarker3'), vals, context=context)
|
||||
-
|
||||
I run the wizard
|
||||
-
|
||||
!python {model: validate.account.move}: |
|
||||
self.validate_move(
|
||||
cr, uid, [ref('wiz_unmarker3')], context=context
|
||||
)
|
||||
-
|
||||
Now I checked that my job is done, and the move is still draft
|
||||
-
|
||||
!python {model: account.move}: |
|
||||
from openerp.addons.connector.queue.job import OpenERPJobStorage
|
||||
from openerp.addons.connector.session import ConnectorSession
|
||||
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
storage = OpenERPJobStorage(session)
|
||||
|
||||
move = self.browse(cr, uid, ref('move2'), context=context)
|
||||
myjob = storage.load(move.post_job_uuid)
|
||||
assert myjob.state == 'done', 'Job is in state {0}, should be done'.format(
|
||||
myjob.state
|
||||
)
|
||||
-
|
||||
I check that the move is still draft
|
||||
-
|
||||
!assert {model: account.move, id: move2}:
|
||||
- state == 'draft'
|
||||
1
account_move_batch_validate/tests/__init__.py
Normal file
1
account_move_batch_validate/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_account_move_batch_validate
|
||||
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tests.common import SingleTransactionCase
|
||||
from odoo.addons.queue_job.job import Job
|
||||
|
||||
|
||||
class TestAccountMoveBatchValidate(SingleTransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(TestAccountMoveBatchValidate, self).setUpClass()
|
||||
self.AccountObj = self.env['account.account']
|
||||
self.AccountJournalObj = self.env['account.journal']
|
||||
self.AccountMoveObj = self.env['account.move']
|
||||
self.ValidateAccountMoveObj = self.env['validate.account.move']
|
||||
self.QueueJobObj = self.env['queue.job']
|
||||
|
||||
self.account_type_recv = self.env.ref(
|
||||
'account.data_account_type_receivable')
|
||||
self.account_type_rev = self.env.ref(
|
||||
'account.data_account_type_revenue')
|
||||
|
||||
self.account_recv = self.AccountObj.create({
|
||||
'code': 'RECVT',
|
||||
'name': "Receivable (test)",
|
||||
'reconcile': True,
|
||||
'user_type_id': self.account_type_recv.id,
|
||||
})
|
||||
self.account_sale = self.AccountObj.create({
|
||||
'code': 'SALET',
|
||||
'name': "Receivable (sale)",
|
||||
'reconcile': True,
|
||||
'user_type_id': self.account_type_rev.id,
|
||||
})
|
||||
|
||||
self.sales_journal = self.AccountJournalObj.create({
|
||||
'name': "Sales journal",
|
||||
'code': 'SAJT',
|
||||
'type': 'sale',
|
||||
'default_credit_account_id': self.account_sale.id,
|
||||
'default_debit_account_id': self.account_sale.id,
|
||||
})
|
||||
|
||||
def create_account_move(self, amount):
|
||||
return self.AccountMoveObj.create({
|
||||
'journal_id': self.sales_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': "Receivable line",
|
||||
'account_id': self.account_recv.id,
|
||||
'debit': amount,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': "Sales line",
|
||||
'account_id': self.account_type_rev.id,
|
||||
'credit': amount,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
def create_move_validate_wizard(self, action, eta=None):
|
||||
return self.ValidateAccountMoveObj.create({
|
||||
'asynchronous': True,
|
||||
'action': action,
|
||||
'eta': eta or 0,
|
||||
})
|
||||
|
||||
def test_01_wizard_asynchronous_post(self):
|
||||
"""
|
||||
Create a move and call the validate account move wizard to
|
||||
post it.
|
||||
"""
|
||||
move = self.create_account_move(1000)
|
||||
|
||||
self.assertEquals(move.state, 'draft')
|
||||
|
||||
wizard = self.create_move_validate_wizard('mark')
|
||||
wizard.with_context({
|
||||
'active_ids': [move.id],
|
||||
'automated_test_execute_now': True,
|
||||
}).validate_move()
|
||||
|
||||
job_uuid = move.post_job_uuid
|
||||
|
||||
self.assertTrue(
|
||||
move.to_post, msg="Move should be marked as 'to post'.")
|
||||
|
||||
self.assertTrue(
|
||||
bool(job_uuid), msg="A job should have been assigned to the move.")
|
||||
|
||||
post_job = Job.load(self.env, job_uuid)
|
||||
post_job.perform()
|
||||
|
||||
self.assertEquals(
|
||||
move.state, 'posted', msg="Move should be posted.")
|
||||
|
||||
def test_02_delete_move_before_job_run(self):
|
||||
"""
|
||||
Create a move and call the validate account move wizard to
|
||||
post it, and then delete the move.
|
||||
"""
|
||||
move = self.create_account_move(3000)
|
||||
|
||||
wizard = self.create_move_validate_wizard('mark', eta=1000)
|
||||
wizard.with_context({
|
||||
'active_ids': [move.id],
|
||||
'automated_test_execute_now': True,
|
||||
}).validate_move()
|
||||
|
||||
job_uuid = move.post_job_uuid
|
||||
|
||||
self.assertTrue(
|
||||
bool(job_uuid), msg="The job has not been created.")
|
||||
|
||||
move.unlink()
|
||||
|
||||
post_job = Job.load(self.env, job_uuid)
|
||||
post_job.perform()
|
||||
|
||||
self.assertEquals(
|
||||
post_job.result,
|
||||
u'Nothing to do because the record has been deleted')
|
||||
|
||||
def test_03_mark_and_unmark(self):
|
||||
"""
|
||||
Create a move and call the validate account move wizard to
|
||||
post it. Recall the validate account move wizard to unmark move.
|
||||
"""
|
||||
move = self.create_account_move(3000)
|
||||
|
||||
wizard = self.create_move_validate_wizard('mark', eta=1000)
|
||||
wizard.with_context({
|
||||
'active_ids': [move.id],
|
||||
'automated_test_execute_now': True,
|
||||
}).validate_move()
|
||||
|
||||
mark_job_uuid = move.post_job_uuid
|
||||
|
||||
self.assertTrue(move.to_post)
|
||||
|
||||
wizard = self.create_move_validate_wizard('unmark', eta=1000)
|
||||
wizard.with_context({
|
||||
'active_ids': [move.id],
|
||||
'automated_test_execute_now': True,
|
||||
}).validate_move()
|
||||
|
||||
self.assertFalse(move.to_post)
|
||||
|
||||
job_uuid = move.post_job_uuid
|
||||
|
||||
self.assertEquals(mark_job_uuid, job_uuid)
|
||||
|
||||
post_job = Job.load(self.env, job_uuid)
|
||||
|
||||
self.assertEquals(post_job.state, 'done', msg="Job should be done")
|
||||
self.assertEquals(
|
||||
post_job.result,
|
||||
"Task set to Done because the user unmarked the move.")
|
||||
|
||||
self.assertEquals(
|
||||
move.state, 'draft', msg="Move should be in 'draft' state")
|
||||
33
account_move_batch_validate/views/account_move.xml
Normal file
33
account_move_batch_validate/views/account_move.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2014 Camptocamp SA, 2017 ACSONE SA/NV
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="account_move_form">
|
||||
<field name="name">account.move.form in (account_move_batch_validate)</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<field name="ref" position="after">
|
||||
<field name="to_post"/>
|
||||
</field>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="account_move_tree">
|
||||
<field name="name">account.move.tree in (account_move_batch_validate)</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<field name="state" position="after">
|
||||
<field name="to_post"/>
|
||||
</field>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,22 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
# #
|
||||
# Author: Leonardo Pistone
|
||||
# Copyright 2014 Camptocamp SA
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU Affero General Public License as #
|
||||
# published by the Free Software Foundation, either version 3 of the #
|
||||
# License, or (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU Affero General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Affero General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
###############################################################################
|
||||
"""Wizard to mark account moves for batch posting."""
|
||||
from . import move_marker # noqa
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_move_validate
|
||||
|
||||
62
account_move_batch_validate/wizard/account_move_validate.py
Normal file
62
account_move_batch_validate/wizard/account_move_validate.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 Camptocamp SA, 2017 ACSONE
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.queue_job.job import job
|
||||
|
||||
|
||||
class AccountMoveValidate(models.TransientModel):
|
||||
|
||||
_inherit = 'validate.account.move'
|
||||
|
||||
action = fields.Selection(
|
||||
selection='_get_actions', string="Action",
|
||||
required=True, default='mark')
|
||||
eta = fields.Integer(string="Seconds to wait before starting the jobs")
|
||||
asynchronous = fields.Boolean(string="Use asynchronous validation")
|
||||
|
||||
@api.model
|
||||
def _get_actions(self):
|
||||
return [
|
||||
('mark', 'Mark for posting'),
|
||||
('unmark', 'Unmark for posting')
|
||||
]
|
||||
|
||||
@api.multi
|
||||
def validate_move(self):
|
||||
self.ensure_one()
|
||||
|
||||
if not self.asynchronous:
|
||||
return super(AccountMoveValidate, self).validate_move()
|
||||
|
||||
wizard_data = {
|
||||
'move_ids': self.env.context.get('active_ids'),
|
||||
'action': self.action,
|
||||
'asynchronous': self.asynchronous,
|
||||
'eta': self.eta,
|
||||
}
|
||||
|
||||
if self.env.context.get('automated_test_execute_now'):
|
||||
return self.process_wizard(wizard_data)
|
||||
else:
|
||||
return self.env[self._name].with_delay(priority=5).process_wizard(
|
||||
wizard_data)
|
||||
|
||||
@job()
|
||||
def process_wizard(self, wizard_data):
|
||||
AccountMoveObj = self.env['account.move']
|
||||
|
||||
move_ids = wizard_data.get('move_ids')
|
||||
action = wizard_data.get('action')
|
||||
eta = wizard_data.get('eta')
|
||||
|
||||
moves = AccountMoveObj.search([
|
||||
('id', 'in', move_ids),
|
||||
('state', '=', 'draft')
|
||||
])
|
||||
|
||||
if action == 'mark':
|
||||
moves.mark_for_posting(eta=eta)
|
||||
elif action == 'unmark':
|
||||
moves.unmark_for_posting()
|
||||
24
account_move_batch_validate/wizard/account_move_validate.xml
Normal file
24
account_move_batch_validate/wizard/account_move_validate.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2014 Camptocamp SA, 2017 ACSONE SA/NV
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record model="ir.ui.view" id="validate_account_move_form">
|
||||
<field name="name">validate.account.move.form (in account_move_batch_validate)</field>
|
||||
<field name="model">validate.account.move</field>
|
||||
<field name="inherit_id" ref="account.validate_account_move_view"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//footer" position="before">
|
||||
<group>
|
||||
<field name="asynchronous"/>
|
||||
<field name="action" attrs="{'invisible': [('asynchronous', '=', False)]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,127 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
# #
|
||||
# Author: Leonardo Pistone
|
||||
# Copyright 2014 Camptocamp SA
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU Affero General Public License as #
|
||||
# published by the Free Software Foundation, either version 3 of the #
|
||||
# License, or (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU Affero General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Affero General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
###############################################################################
|
||||
"""Wizards for batch posting."""
|
||||
|
||||
import logging
|
||||
|
||||
from openerp.osv import fields, orm
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from openerp.addons.connector.session import ConnectorSession
|
||||
from openerp.addons.connector.queue.job import job
|
||||
except ImportError:
|
||||
_logger.debug('Can not `import connector`.')
|
||||
|
||||
def empty_decorator(func):
|
||||
return func
|
||||
job = empty_decorator
|
||||
|
||||
|
||||
class ValidateAccountMove(orm.TransientModel):
|
||||
|
||||
"""Wizard to mark account moves for batch posting."""
|
||||
|
||||
_inherit = "validate.account.move"
|
||||
|
||||
_columns = {
|
||||
'action': fields.selection([('mark', 'Mark for posting'),
|
||||
('unmark', 'Unmark for posting')],
|
||||
"Action", required=True),
|
||||
'eta': fields.integer('Seconds to wait before starting the jobs'),
|
||||
'asynchronous': fields.boolean('Use asynchronous validation'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'action': 'mark',
|
||||
'asynchronous': True,
|
||||
}
|
||||
|
||||
def validate_move(self, cr, uid, ids, context=None):
|
||||
"""Create a single job that will create one job per move.
|
||||
|
||||
Return action.
|
||||
|
||||
"""
|
||||
session = ConnectorSession(cr, uid, context=context)
|
||||
wizard_id = ids[0]
|
||||
# to find out what _classic_write does, read the documentation.
|
||||
wizard_data = self.read(cr, uid, wizard_id, context=context,
|
||||
load='_classic_write')
|
||||
if not wizard_data.get('asynchronous'):
|
||||
return super(ValidateAccountMove, self)\
|
||||
.validate_move(cr, uid, ids, context=context)
|
||||
wizard_data.pop('id')
|
||||
if wizard_data.get('journal_ids'):
|
||||
journals_ids_vals = [(6, False,
|
||||
wizard_data.get('journal_ids'))]
|
||||
wizard_data['journal_ids'] = journals_ids_vals
|
||||
if wizard_data.get('period_ids'):
|
||||
periods_ids_vals = [(6, False,
|
||||
wizard_data.get('period_ids'))]
|
||||
wizard_data['period_ids'] = periods_ids_vals
|
||||
|
||||
if context.get('automated_test_execute_now'):
|
||||
process_wizard(session, self._name, wizard_data)
|
||||
else:
|
||||
process_wizard.delay(session, self._name, wizard_data)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def process_wizard(self, cr, uid, ids, context=None):
|
||||
"""Choose the correct list of moves to mark and then validate."""
|
||||
for wiz in self.browse(cr, uid, ids, context=context):
|
||||
|
||||
move_obj = self.pool['account.move']
|
||||
|
||||
domain = [('state', '=', 'draft'),
|
||||
('journal_id', 'in', wiz.journal_ids.ids),
|
||||
('period_id', 'in', wiz.period_ids.ids)]
|
||||
move_ids = move_obj.search(cr, uid, domain, order='date',
|
||||
context=context)
|
||||
|
||||
if wiz.action == 'mark':
|
||||
move_obj.mark_for_posting(cr, uid, move_ids, eta=wiz.eta,
|
||||
context=context)
|
||||
|
||||
elif wiz.action == 'unmark':
|
||||
move_obj.unmark_for_posting(cr, uid, move_ids, context=context)
|
||||
|
||||
|
||||
@job
|
||||
def process_wizard(session, model_name, wizard_data):
|
||||
"""Create jobs to validate Journal Entries."""
|
||||
|
||||
wiz_obj = session.pool[model_name]
|
||||
new_wiz_id = wiz_obj.create(
|
||||
session.cr,
|
||||
session.uid,
|
||||
wizard_data,
|
||||
session.context
|
||||
)
|
||||
|
||||
wiz_obj.process_wizard(
|
||||
session.cr,
|
||||
session.uid,
|
||||
ids=[new_wiz_id],
|
||||
context=session.context,
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="validate_account_move_view" model="ir.ui.view">
|
||||
<field name="name">Post Journal Entries</field>
|
||||
<field name="model">validate.account.move</field>
|
||||
<field name="inherit_id" ref="account.validate_account_move_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='journal_ids']" position="before">
|
||||
<field name="asynchronous" />
|
||||
<field name="action" attrs="{'invisible': [('asynchronous', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account.menu_validate_account_moves" model="ir.ui.menu">
|
||||
<field name="name">Post Journal Entries</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user