Merge lp:~pexego/account-financial-tools/adding_account_tools_from_extra_addons_to into lp:~account-core-editors/account-financial-tools/6.1

Proposed by Omar (Pexego)
Status: Rejected
Rejected by: Joël Grand-Guillaume @ camptocamp
Proposed branch: lp:~pexego/account-financial-tools/adding_account_tools_from_extra_addons_to
Merge into: lp:~account-core-editors/account-financial-tools/6.1
Diff against target: 12097 lines (+11835/-0)
51 files modified
account_admin_tools/__init__.py (+35/-0)
account_admin_tools/__openerp__.py (+86/-0)
account_admin_tools/account_chart_checker.py (+211/-0)
account_admin_tools/account_chart_checker.xml (+54/-0)
account_admin_tools/account_importer.py (+261/-0)
account_admin_tools/account_importer.xml (+63/-0)
account_admin_tools/account_importer_wizard.py (+268/-0)
account_admin_tools/account_importer_wizard.xml (+62/-0)
account_admin_tools/account_move_importer.py (+300/-0)
account_admin_tools/account_move_importer.xml (+75/-0)
account_admin_tools/account_move_importer_wizard.py (+266/-0)
account_admin_tools/account_move_importer_wizard.xml (+74/-0)
account_admin_tools/admin_tools_menu.xml (+20/-0)
account_admin_tools/move_partner_account.py (+188/-0)
account_admin_tools/move_partner_account.xml (+58/-0)
account_admin_tools/move_partner_account_wizard.py (+145/-0)
account_admin_tools/move_partner_account_wizard.xml (+42/-0)
account_admin_tools/revalidate_moves.py (+157/-0)
account_admin_tools/revalidate_moves.xml (+82/-0)
account_admin_tools/revalidate_moves_wizard.py (+100/-0)
account_admin_tools/revalidate_moves_wizard.xml (+45/-0)
account_admin_tools/set_invoice_ref_in_moves.py (+205/-0)
account_admin_tools/set_invoice_ref_in_moves.xml (+82/-0)
account_admin_tools/set_partner_in_moves.py (+214/-0)
account_admin_tools/set_partner_in_moves.xml (+82/-0)
account_chart_update/__init__.py (+28/-0)
account_chart_update/__openerp__.py (+64/-0)
account_chart_update/account.py (+1329/-0)
account_chart_update/account_view.xml (+159/-0)
account_chart_update/i18n/account_chart_update.pot (+744/-0)
account_chart_update/i18n/ca.po (+835/-0)
account_chart_update/i18n/ca_ES.po (+323/-0)
account_chart_update/i18n/es.po (+841/-0)
account_chart_update/i18n/es_ES.po (+770/-0)
account_chart_update/i18n/pt.po (+844/-0)
account_chart_update/i18n/sv.po (+765/-0)
account_renumber/__init__.py (+28/-0)
account_renumber/__openerp__.py (+52/-0)
account_renumber/i18n/account_renumber.pot (+148/-0)
account_renumber/i18n/bg.po (+154/-0)
account_renumber/i18n/ca.po (+175/-0)
account_renumber/i18n/ca_ES.po (+161/-0)
account_renumber/i18n/es.po (+171/-0)
account_renumber/i18n/es_ES.po (+159/-0)
account_renumber/i18n/gl.po (+170/-0)
account_renumber/i18n/pt.po (+172/-0)
account_renumber/i18n/sv.po (+153/-0)
account_renumber/test/create_moves.py (+113/-0)
account_renumber/wizard/__init__.py (+29/-0)
account_renumber/wizard/wizard_renumber.py (+220/-0)
account_renumber/wizard/wizard_renumber_view.xml (+53/-0)
To merge this branch: bzr merge lp:~pexego/account-financial-tools/adding_account_tools_from_extra_addons_to
Reviewer Review Type Date Requested Status
Joël Grand-Guillaume @ camptocamp Disapprove
Review via email: mp+138676@code.launchpad.net

Description of the change

Moving pxgo_account_admin_tools, account_renumber and account_chart_update from extra-addons with small improvements:

pxgo_account_admin_tools renamed to account_admin_tools
pxgo_account_admin_tools and account_renumber ported to 6.1
PEP8

To post a comment you must log in.
Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Dear Omar,

First of all thank you for your contribution on this project ! Happy to see them landing here. I have analyzed your module here, more from a functional / features point of view, rather than technical, but still I have some remarks:

 * account_chart_update : Seems the right place to land. The menu where it stands is good for me as it'll be the same people that will use it than the one who will create the account chart.

 * account_renumber : Seems useful in some cases for an administrator of the system, but it could definitely be dangerous for a lambda user. Move number are now related to invoice when they're issue by them. So using this by a non-administrator could lead to dangerous result. I would really prefer to have the menu entry in the administration menu for that purpose.

 * account_admin_tools : On this one, my opinion is that the import feature (for both account and move) should be made in a new module. No need to mix accounting importation features with the "repair" tools. Then, for the repair tools, as far as I can see, it's more repair tools used on past version (I say version 5.0 probably, but I'm may be wrong). Most of the provided features (but may be not all) are a bit obsolete from my point of view.

What I would suggest to you if, you agree, is to:

 * Make a merge proposal for both account_chart_update and account_renumber and moving the menu entry

 * Extract the importation tool form the account_admin_tools in a new module and make a MP for it in this project

 * For the remaining "check and repair" tools, may be we need opinion from others as well. On my side, I don't think it's a good module to include here. Or at least not with the current included feature.

Other comments welcome here. We also said that serie 6.1 should include old modules, so if you think I'm to "hard" here, please let me know. For now, I put my vote on disapprove, but I'm open to discussion here.

In any case, thank you for your MP !

Regards,

Joël

review: Disapprove
Revision history for this message
Santi Argüeso(Pexego) (santiago-pexego) wrote :

Just a little explanation:

The case of account_renumber is something special. In spanish localization, move number is not related to invoice number and is normal for accountants renumber theses moves ata the end of the period of year . So i think we have a conflict here. I'm not sure wich could be the best solution, maybe Include this module in spanish localization if you think that is not usefull for other accounting sistems

I totally agree with you about account_admin_tools, but for 6.1 i think is not a god idea split it into 2 modules. For sure it must be done for 7.0 and remove check and repair functionality

Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi Santi,

Thanks for the explanation on account_renumber. An easy solution will be to put the default menu in Administration and add one in your spanish localization in the account section, what do you think ?

For the account_admin_tools, you still using those "repair and check" fixes on version 6.1 ? Having a look on LP show that those trouble was discussed in 2010 and I can't remember that we face it in v6.1.

I don't want to be to restrictive, but honestly, I don't want to include a module in which we have >70% of the code that will not be used, what do you think ?

Other though here ?

Regards,

Joël

Revision history for this message
Santi Argüeso(Pexego) (santiago-pexego) wrote :

Hi Joël,

First of all, thanks a lot for your incredible work.

We can do what you propose for account_renumber. That's a good idea.

And yes, I think we are not using this tools in v6.1. I can not say if anybody use it (anyone?) but we can do what you propose, we can live with this. I don't think at all you are being restrictive, you are doing the best for all community to have a good set of modules. Thanks again.

Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi Santi,

So, I mark this merge as rejected to have a better visibility. We start to have quite a lot a review to do ;) Don't hesitate to resubmit.

Thanks for your understanding, have a nice end of year !

Regards,

Joël

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'account_admin_tools'
2=== added file 'account_admin_tools/__init__.py'
3--- account_admin_tools/__init__.py 1970-01-01 00:00:00 +0000
4+++ account_admin_tools/__init__.py 2012-12-07 10:00:37 +0000
5@@ -0,0 +1,35 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# OpenERP, Open Source Management Solution
10+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
11+# $Id$
12+#
13+# This program is free software: you can redistribute it and/or modify
14+# it under the terms of the GNU Affero General Public License as published
15+# by the Free Software Foundation, either version 3 of the License, or
16+# (at your option) any later version.
17+#
18+# This program is distributed in the hope that it will be useful,
19+# but WITHOUT ANY WARRANTY; without even the implied warranty of
20+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+# GNU Affero General Public License for more details.
22+#
23+# You should have received a copy of the GNU Affero General Public License
24+# along with this program. If not, see <http://www.gnu.org/licenses/>.
25+#
26+##############################################################################
27+
28+"""
29+Account Admin Tools
30+"""
31+
32+__author__ = "Borja López Soilán (Pexego)"
33+
34+import account_importer
35+import account_move_importer
36+import account_chart_checker
37+import revalidate_moves
38+import move_partner_account
39+import set_partner_in_moves
40+import set_invoice_ref_in_moves
41
42=== added file 'account_admin_tools/__openerp__.py'
43--- account_admin_tools/__openerp__.py 1970-01-01 00:00:00 +0000
44+++ account_admin_tools/__openerp__.py 2012-12-07 10:00:37 +0000
45@@ -0,0 +1,86 @@
46+# -*- coding: utf-8 -*-
47+##############################################################################
48+#
49+# OpenERP, Open Source Management Solution
50+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
51+# $Id$
52+#
53+# This program is free software: you can redistribute it and/or modify
54+# it under the terms of the GNU Affero General Public License as published
55+# by the Free Software Foundation, either version 3 of the License, or
56+# (at your option) any later version.
57+#
58+# This program is distributed in the hope that it will be useful,
59+# but WITHOUT ANY WARRANTY; without even the implied warranty of
60+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
61+# GNU Affero General Public License for more details.
62+#
63+# You should have received a copy of the GNU Affero General Public License
64+# along with this program. If not, see <http://www.gnu.org/licenses/>.
65+#
66+##############################################################################
67+
68+{
69+ "name": "Account Admin Tools",
70+ "version": "6.1",
71+ "author": "Pexego",
72+ "website": "http://www.pexego.es",
73+ "category": "Enterprise Specific Modules",
74+ "description": """Account Tools for Administrators
75+
76+Import tools:
77+
78+- Import accounts from CSV files. This may be useful to import the initial
79+ accounts into OpenERP.
80+
81+- Import account moves from CSV files. This may be useful to import the initial
82+ balance into OpenERP.
83+
84+
85+Check and Repair tools:
86+
87+- Check the Chart of Accounts for problems in its structure. This will allow
88+ you to detect incoherences like the ones caused by bugs like
89+ https://bugs.launchpad.net/openobject-server/+bug/581137
90+ (the preordered tree [parent_left/parent_right] not matching the
91+ parent-child structure [parent_id]).
92+
93+- Revalidate confirmed account moves so their analytic lines are regenerated.
94+ This may be used to fix the data after bugs like
95+ https://bugs.launchpad.net/openobject-addons/+bug/582988
96+ The wizard also lets you find account moves missing their analytic lines.
97+
98+- Set the receivable/payable account of the partners, in moves and invoices
99+ where a generic receivable/payable account was used instead.
100+
101+- Set the parent reference in account move lines where the receivable/payable
102+ account associated with the partner was used, but a partner reference wasn't
103+ set. This may fix cases where the receivable/payable amounts displayed in the
104+ partner form does not match the balance of the receivable/payable accounts.
105+
106+- Set the reference in account moves, associated with invoices, that do not
107+ have the right reference (the reference from the invoice if it was a supplier
108+ invoice, or the number from the invoice if it was a customer invoice).
109+ This is useful to fix the account moves after changing the invoice
110+ references.
111+ """,
112+ "depends": [
113+ 'base',
114+ 'account',
115+ ],
116+ "init_xml": [],
117+ "demo_xml": [],
118+ "update_xml": [
119+ 'admin_tools_menu.xml',
120+ 'account_importer.xml',
121+ 'account_move_importer.xml',
122+ 'account_chart_checker.xml',
123+ 'revalidate_moves.xml',
124+ 'move_partner_account.xml',
125+ 'set_partner_in_moves.xml',
126+ 'set_invoice_ref_in_moves.xml',
127+ ],
128+ "installable": True,
129+ 'active': False
130+
131+}
132
133=== added file 'account_admin_tools/account_chart_checker.py'
134--- account_admin_tools/account_chart_checker.py 1970-01-01 00:00:00 +0000
135+++ account_admin_tools/account_chart_checker.py 2012-12-07 10:00:37 +0000
136@@ -0,0 +1,211 @@
137+# -*- coding: utf-8 -*-
138+##############################################################################
139+#
140+# OpenERP, Open Source Management Solution
141+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
142+# $Id$
143+#
144+# This program is free software: you can redistribute it and/or modify
145+# it under the terms of the GNU Affero General Public License as published
146+# by the Free Software Foundation, either version 3 of the License, or
147+# (at your option) any later version.
148+#
149+# This program is distributed in the hope that it will be useful,
150+# but WITHOUT ANY WARRANTY; without even the implied warranty of
151+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
152+# GNU Affero General Public License for more details.
153+#
154+# You should have received a copy of the GNU Affero General Public License
155+# along with this program. If not, see <http://www.gnu.org/licenses/>.
156+#
157+##############################################################################
158+"""
159+Account Chart Checker Wizard
160+"""
161+__author__ = "Borja López Soilán (Pexego)"
162+
163+import re
164+from osv import fields, osv
165+from tools.translate import _
166+
167+
168+class account_chart_checker_problem(osv.osv_memory):
169+ """
170+ A problem found in the account chart
171+ """
172+ _name = "account_admin_tools.account_chart_checker_problem"
173+ _description = "Account Chart Problem"
174+
175+ _columns = {
176+ 'wizard_id': fields.many2one('account_admin_tools.account_chart_checker',
177+ 'Wizard', required=True, readonly=True),
178+ 'account_id': fields.many2one('account.account', 'Account', required=True,
179+ readonly=True),
180+ 'severity': fields.selection([('informative', 'Informative'),
181+ ('low', 'Low'), ('medium', 'Medium'), ('high', 'High')],
182+ 'Severity', readonly=True),
183+ 'problem': fields.selection([
184+ ('not_parent_of_children', 'Not parent of its children'),
185+ ('not_children_of_parent', 'Not children of its parent'),
186+ ], 'Problem', readonly=True),
187+ 'description': fields.text('Description')
188+ }
189+
190+ def search(self, cr, uid, args, offset=0, limit=None, order=None,
191+ context=None, count=False):
192+ """
193+ Redefinition of the search method (as osv_memory wizards currently
194+ don't support domain filters by themselves.
195+ """
196+ problem_ids = super(account_chart_checker_problem, self).search(cr,
197+ uid, args, offset=offset, limit=limit, order=order,
198+ context=context, count=count)
199+
200+ for arg in args:
201+ if arg[0] == 'wizard_id' and arg[1] == '=':
202+ wizard_id = arg[2]
203+ problems = self.browse(cr, uid, problem_ids, context=context)
204+ problem_ids = [problem.id for problem in problems
205+ if problem.wizard_id.id == wizard_id]
206+
207+ return problem_ids
208+
209+
210+account_chart_checker_problem()
211+
212+
213+class account_chart_checker(osv.osv_memory):
214+ """
215+ Account Chart Checker
216+ """
217+ _name = "account_admin_tools.account_chart_checker"
218+ _description = "Account Chart Checker Wizard"
219+
220+ _columns = {
221+ 'company_id': fields.many2one('res.company', 'Company',
222+ required=True, readonly=True),
223+ 'problem_ids': fields.one2many('account_admin_tools.account_chart_checker_problem',
224+ 'wizard_id', 'Problems'),
225+ 'state': fields.selection([('new', 'New'), ('done', 'Done')],
226+ 'Status', readonly=True),
227+ }
228+
229+ _defaults = {
230+ 'state': lambda *a: 'new',
231+ 'company_id': lambda self, cr, uid, context:\
232+ self.pool.get('res.users').browse(cr, uid, uid,\
233+ context).company_id.id,
234+ }
235+
236+ def action_check(self, cr, uid, ids, context=None):
237+ """
238+ Checks the account chart and reports the problems it finds.
239+ """
240+ for wiz in self.browse(cr, uid, ids, context):
241+ problems = []
242+
243+ account_ids = self.pool.get('account.account').search(cr,
244+ uid, [], context=context)
245+
246+ for account in self.pool.get('account.account').browse(cr,
247+ uid, account_ids, context=context):
248+ self._check_parent_of_children(cr, uid, account, problems)
249+ self._check_child_of_parent(cr, uid, account, problems)
250+
251+ self.write(cr, uid, [wiz.id], {
252+ 'problem_ids': [(0, 0, problem)
253+ for problem in problems] or None,
254+ 'state': 'done'
255+ })
256+
257+ #
258+ # Return the next view: Show the problems
259+ #
260+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
261+ ('model', '=', 'ir.ui.view'),
262+ ('module', '=', 'account_admin_tools'),
263+ ('name', '=', 'account_chart_checker_problem_tree')
264+ ])
265+ resource_id = self.pool.get('ir.model.data').read(cr, uid,
266+ model_data_ids, fields=['res_id'], context=context)[0]['res_id']
267+
268+ return {
269+ 'name': _("Problems Found in the Chart of Accounts"),
270+ 'type': 'ir.actions.act_window',
271+ 'res_model': 'account_admin_tools.account_chart_checker_problem',
272+ 'view_type': 'form',
273+ 'view_mode': 'tree',
274+ 'views': [(resource_id, 'tree')],
275+ 'domain': "[('wizard_id', '=', %s)]" % wiz.id,
276+ 'context': context,
277+ }
278+
279+ def _check_parent_of_children(self, cr, uid, account, problems=[],
280+ context=None):
281+ """
282+ Checks that for a parent account, every children has that account
283+ as its parent.
284+ """
285+ query = """
286+ SELECT id FROM account_account WHERE parent_left > %s
287+ and parent_right < %s AND COALESCE(parent_id,0) NOT IN
288+ (SELECT id FROM account_account WHERE parent_left > %s
289+ and parent_right < %s) AND COALESCE(parent_id,0) != %s
290+ """
291+ cr.execute(query, (account.parent_left or 0, account.parent_right
292+ or 0, account.parent_left or 0, account.parent_right or 0,
293+ account.id))
294+ problematic_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
295+
296+ for child in self.pool.get('account.account').browse(cr, uid,
297+ problematic_ids, context=context):
298+ problems.append({
299+ 'problem': 'not_parent_of_children',
300+ 'severity': 'high',
301+ 'account_id': account.id,
302+ 'description': _('The account %d (%s) is listed as \
303+ children of %d (%s) in the preordered tree, \
304+ but its parent is %d (%s)')
305+ % (child.id, child.code, account.id,
306+ account.code, child.parent_id and
307+ child.parent_id.id, child.parent_id
308+ and child.parent_id.code)
309+ })
310+
311+ def _check_child_of_parent(self, cr, uid, account, problems=[],
312+ context=None):
313+ """
314+ Checks that for a child account, his parent has that account
315+ in its children.
316+ """
317+ query = """
318+ SELECT id FROM account_account WHERE parent_left < %s
319+ and parent_right > %s
320+ """
321+ cr.execute(query, (account.parent_left or 0, account.parent_right
322+ or 0))
323+ parent_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
324+
325+ if account.parent_id and (account.parent_id.id not in parent_ids):
326+ problems.append({
327+ 'problem': 'not_children_of_parent',
328+ 'severity': 'high',
329+ 'account_id': account.id,
330+ 'description': _('The account %d (%s) is children of %d\
331+ (%s), but is not listed as its children on\
332+ the preordered tree')
333+ % (account.id, account.code,
334+ account.parent_id.id, account.parent_id.code)
335+ })
336+ elif parent_ids and not account.parent_id:
337+ problems.append({
338+ 'problem': 'not_children_of_parent',
339+ 'severity': 'high',
340+ 'account_id': account.id,
341+ 'description': _('The account %d (%s) is a top level account,\
342+ but is listed as a child on the preordered tree')
343+ % (account.id, account.code)
344+ })
345+
346+
347+account_chart_checker()
348
349=== added file 'account_admin_tools/account_chart_checker.xml'
350--- account_admin_tools/account_chart_checker.xml 1970-01-01 00:00:00 +0000
351+++ account_admin_tools/account_chart_checker.xml 2012-12-07 10:00:37 +0000
352@@ -0,0 +1,54 @@
353+<?xml version="1.0" encoding="utf-8"?>
354+<openerp>
355+ <data>
356+ <record id="view_account_chart_checker_form" model="ir.ui.view">
357+ <field name="name">account_chart_checker.form</field>
358+ <field name="model">account_admin_tools.account_chart_checker</field>
359+ <field name="type">form</field>
360+ <field name="arch" type="xml">
361+ <form string="Check the Chart of Accounts">
362+ <label string="This wizard will search for problems in the Chart of Accounts:" colspan="4"/>
363+ <label string="" colspan="4"/>
364+ <label string="- It will verify that the preordered tree of accounts, that OpenERP uses to calculate the amounts, matches the parent-children structure" colspan="4"/>
365+ <label string="" colspan="4"/>
366+ <label string="A list with the problems found (if any) will be shown afterwards." colspan="4"/>
367+ <label string="" colspan="4"/>
368+ <group colspan="4">
369+ <button string="Cancel" special="cancel" icon="gtk-cancel" states="new"/>
370+ <button string="Check" name="action_check" type="object" icon="gtk-apply" states="new"/>
371+ </group>
372+
373+ <field name="state" invisible="1"/>
374+ </form>
375+ </field>
376+ </record>
377+
378+ <record id="account_chart_checker_problem_tree" model="ir.ui.view">
379+ <field name="name">account_chart_checker_problem.tree</field>
380+ <field name="model">account_admin_tools.account_chart_checker_problem</field>
381+ <field name="type">tree</field>
382+ <field name="arch" type="xml">
383+ <tree string="Problems" colors="red:severity=='high';green:severity=='informative'">
384+ <field name="problem"/>
385+ <field name="severity"/>
386+ <field name="account_id"/>
387+ <field name="description"/>
388+ </tree>
389+ </field>
390+ </record>
391+
392+ <record id="action_account_chart_checker" model="ir.actions.act_window">
393+ <field name="name">Check the Chart of Accounts</field>
394+ <field name="res_model">account_admin_tools.account_chart_checker</field>
395+ <field name="view_type">form</field>
396+ <field name="view_mode">form</field>
397+ <field name="view_id" ref="view_account_chart_checker_form"/>
398+ <field name="target">new</field>
399+ </record>
400+ <menuitem id="menu_action_account_chart_checker"
401+ parent="menu_action_account_admin_tools_repair"
402+ action="action_account_chart_checker"
403+ sequence="10"/>
404+
405+ </data>
406+</openerp>
407
408=== added file 'account_admin_tools/account_importer.py'
409--- account_admin_tools/account_importer.py 1970-01-01 00:00:00 +0000
410+++ account_admin_tools/account_importer.py 2012-12-07 10:00:37 +0000
411@@ -0,0 +1,261 @@
412+# -*- coding: utf-8 -*-
413+##############################################################################
414+#
415+# OpenERP, Open Source Management Solution
416+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
417+# $Id$
418+#
419+# This program is free software: you can redistribute it and/or modify
420+# it under the terms of the GNU Affero General Public License as published
421+# by the Free Software Foundation, either version 3 of the License, or
422+# (at your option) any later version.
423+#
424+# This program is distributed in the hope that it will be useful,
425+# but WITHOUT ANY WARRANTY; without even the implied warranty of
426+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
427+# GNU Affero General Public License for more details.
428+#
429+# You should have received a copy of the GNU Affero General Public License
430+# along with this program. If not, see <http://www.gnu.org/licenses/>.
431+#
432+##############################################################################
433+"""
434+Account Importer
435+"""
436+__author__ = "Borja López Soilán (Pexego)"
437+
438+import logging
439+import time
440+import csv
441+import base64
442+import StringIO
443+import re
444+from osv import fields, osv
445+from tools.translate import _
446+
447+
448+class account_importer(osv.osv_memory):
449+ """
450+ Account Importer
451+
452+ Creates accounts from a CSV file.
453+
454+ The CSV file lines are expected to have at least the code and name of the
455+ account.
456+
457+ The wizard will find the account brothers (or parent) having the same
458+ account code sufix, and will autocomplete the rest of the account
459+ parameters (account type, reconcile, parent account...).
460+
461+ The CSV file lines are tested to be valid account lines using the regular
462+ expresion options of the wizard.
463+ """
464+ _name = "account_admin_tools.account_importer"
465+ _description = "Account importation wizard"
466+
467+ _columns = {
468+ #
469+ # Account move parameters
470+ #
471+ 'company_id': fields.many2one('res.company', 'Company', required=True),
472+ 'overwrite': fields.boolean('Overwrite', help="If the account already\
473+ exists, overwrite its name?"),
474+ #
475+ # Input file
476+ #
477+ 'input_file': fields.binary('File', filters="*.csv", required=True),
478+ 'input_file_name': fields.char('File name', size=256),
479+ 'csv_delimiter': fields.char('Delimiter', size=1, required=True),
480+ 'csv_quotechar': fields.char('Quote', size=1, required=True),
481+ 'csv_code_index': fields.integer('Code field', required=True),
482+ 'csv_code_regexp': fields.char('Code regexp', size=32, required=True),
483+ 'csv_name_index': fields.integer('Name field', required=True),
484+ 'csv_name_regexp': fields.char('Name regexp', size=32, required=True),
485+ }
486+
487+ _defaults = {
488+ 'company_id': lambda self, cr, uid, context:\
489+ self.pool.get('res.users').browse(cr, uid, uid,\
490+ context).company_id.id,
491+ 'csv_delimiter': lambda *a: ';',
492+ 'csv_quotechar': lambda *a: '"',
493+ 'csv_code_index': lambda *a: 0,
494+ 'csv_name_index': lambda *a: 1,
495+ 'csv_code_regexp': lambda *a: r'^[0-9]+$',
496+ 'csv_name_regexp': lambda *a: r'^.*$',
497+ }
498+
499+ def _find_parent_account_id(self, cr, uid, wiz, account_code, context=None):
500+ """
501+ Finds the parent account given an account code.
502+ It will remove the last digit of the code until it finds an
503+ account that matches exactly the code.
504+ """
505+ if len(account_code) > 0:
506+ parent_account_code = account_code[:-1]
507+ while len(parent_account_code) > 0:
508+ account_ids = self.pool.get('account.account').search(cr,\
509+ uid, [('code', '=', parent_account_code),
510+ ('company_id', '=', wiz.company_id.id)
511+ ])
512+ if account_ids and len(account_ids) > 0:
513+ return account_ids[0]
514+ parent_account_code = parent_account_code[:-1]
515+ # No parent found
516+ return None
517+
518+ def _find_brother_account_id(self, cr, uid, wiz, account_code, context=None):
519+ """
520+ Finds a brother account given an account code.
521+ It will remove the last digit of the code until it finds an
522+ account that matches the begin of the code.
523+ """
524+ if len(account_code) > 0:
525+ brother_account_code = account_code[:-1]
526+ while len(brother_account_code) > 0:
527+ account_ids = self.pool.get('account.account').search(cr,\
528+ uid, [
529+ ('code', '=like', brother_account_code + '%%'),
530+ ('company_id', '=', wiz.company_id.id)
531+ ])
532+ if account_ids and len(account_ids) > 0:
533+ return account_ids[0]
534+ brother_account_code = brother_account_code[:-1]
535+ # No brother found
536+ return None
537+
538+ def action_import(self, cr, uid, ids, context=None):
539+ """
540+ Imports the accounts from the CSV file using the options from the
541+ wizard.
542+ """
543+ # List of the imported accounts
544+ imported_account_ids = []
545+
546+ logger = logging.getLogger("account_importer")
547+ for wiz in self.browse(cr, uid, ids, context):
548+ if not wiz.input_file:
549+ raise osv.except_osv(_('UserError'),\
550+ _("You need to select a file!"))
551+
552+ # Decode the file data
553+ data = base64.b64decode(wiz.input_file)
554+
555+ #
556+ # Read the file
557+ #
558+ reader = csv.reader(StringIO.StringIO(data),
559+ delimiter=str(wiz.csv_delimiter),
560+ quotechar=str(wiz.csv_quotechar))
561+
562+ for record in reader:
563+ # Ignore short records
564+ if len(record) > wiz.csv_code_index \
565+ and len(record) > wiz.csv_name_index:
566+
567+ record_code = record[wiz.csv_code_index]
568+ record_name = record[wiz.csv_name_index]
569+
570+ #
571+ # Ignore invalid records
572+ #
573+ if re.match(wiz.csv_code_regexp, record_code) \
574+ and re.match(wiz.csv_name_regexp, record_name):
575+
576+ #
577+ # Search for the account
578+ #
579+ account_ids = self.pool.get('account.account').search(cr,\
580+ uid, [
581+ ('code', '=', record_code),
582+ ('company_id', '=', wiz.company_id.id)
583+ ])
584+ if account_ids:
585+ if wiz.overwrite:
586+ logger.debug("Overwriting \
587+ account: %s %s" % (record_code,\
588+ record_name))
589+ self.pool.get('account.account').write(cr,\
590+ uid, account_ids, {
591+ 'name': record_name
592+ })
593+ imported_account_ids.extend(account_ids)
594+ else:
595+ #
596+ # Find the account's parent
597+ #
598+ parent_account_id = self._find_parent_account_id(cr,\
599+ uid, wiz, record_code)
600+
601+ if not parent_account_id:
602+ logger.warning("Couldn't find a\
603+ parent account for: %s" % record_code)
604+
605+ #
606+ # Find the account's brother
607+ # (will be used as template)
608+ #
609+ brother_account_id = self._find_brother_account_id(cr,\
610+ uid, wiz, record_code)
611+
612+ if not brother_account_id:
613+ logger.warning("Couldn't find a\
614+ brother account for: %s" % record_code)
615+
616+ brother_account = self.pool.get('account.account').browse(cr, \
617+ uid, brother_account_id)
618+
619+ #
620+ # Create the new account
621+ #
622+ logger.debug("Creating new account:\
623+ %s %s" % (record_code, record_name))
624+ account_id = self.pool.get('account.account').create(cr,\
625+ uid, {
626+ 'code': record_code,
627+ 'name': record_name,
628+ 'parent_id': parent_account_id,
629+ 'type': brother_account.type,
630+ 'user_type': brother_account.user_type.id,
631+ 'reconcile': brother_account.reconcile,
632+ 'company_id': wiz.company_id.id,
633+ 'currency_id': brother_account.currency_id.id,
634+ 'currency_mode': brother_account.currency_mode,
635+ 'active': 1,
636+ 'tax_ids': [(6, 0, [tax.id for \
637+ tax in brother_account.tax_ids])],
638+ 'note': False,
639+ })
640+
641+ imported_account_ids.append(account_id)
642+ else:
643+ logger.warning("Invalid record format\
644+ (ignoring line): %s" % repr(record))
645+ else:
646+ logger.warning("Too short record \
647+ (ignoring line): %s" % repr(record))
648+
649+ #
650+ # Show the accounts to the user
651+ #
652+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
653+ ('model', '=', 'ir.ui.view'),
654+ ('module', '=', 'account'),
655+ ('name', '=', 'view_account_form')
656+ ])
657+ resource_id = self.pool.get('ir.model.data').read(cr, uid,\
658+ model_data_ids, fields=['res_id'], context=context)[0]['res_id']
659+
660+ return {
661+ 'name': _("Imported accounts"),
662+ 'type': 'ir.actions.act_window',
663+ 'res_model': 'account.account',
664+ 'view_type': 'form',
665+ 'view_mode': 'tree,form',
666+ 'views': [(False, 'tree'), (resource_id, 'form')],
667+ 'domain': "[('id', 'in', %s)]" % imported_account_ids,
668+ 'context': context,
669+ }
670+
671+
672+account_importer()
673
674=== added file 'account_admin_tools/account_importer.xml'
675--- account_admin_tools/account_importer.xml 1970-01-01 00:00:00 +0000
676+++ account_admin_tools/account_importer.xml 2012-12-07 10:00:37 +0000
677@@ -0,0 +1,63 @@
678+<?xml version="1.0" encoding="utf-8"?>
679+<openerp>
680+ <data>
681+
682+ <record id="view_account_importer_form" model="ir.ui.view">
683+ <field name="name">account_importer.form</field>
684+ <field name="model">account_admin_tools.account_importer</field>
685+ <field name="type">form</field>
686+ <field name="arch" type="xml">
687+ <form string="Account importer">
688+ <label string="This wizard will import accounts from a CSV file." colspan="4"/>
689+ <label string="Only the account code and name are needed, the rest of the required account data will be filled based on its brother accounts (same code begining)." colspan="4"/>
690+ <label string="" colspan="4"/>
691+ <newline/>
692+ <group string="Account parameters" colspan="4">
693+ <label string="Select the parameters for the account"/>
694+ <group colspan="4">
695+ <field name="company_id"/>
696+ <field name="overwrite"/>
697+ </group>
698+ </group>
699+ <group string="Input file" colspan="4">
700+ <label string="Select the CSV file with the lines for the account move"/>
701+ <group colspan="4">
702+ <field name="input_file_name" string="File"/>
703+ <field name="input_file" filename="input_file_name" nolabel="1"/>
704+ <group colspan="2">
705+ <separator string="File format" colspan="4"/>
706+ <field name="csv_delimiter"/>
707+ <field name="csv_quotechar"/>
708+ </group>
709+ <group colspan="2">
710+ <separator string="Record format" colspan="4"/>
711+ <field name="csv_code_index"/>
712+ <field name="csv_code_regexp"/>
713+ <field name="csv_name_index"/>
714+ <field name="csv_name_regexp"/>
715+ </group>
716+ </group>
717+ </group>
718+ <group colspan="4">
719+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
720+ <button string="Import" name="action_import" type="object" icon="gtk-ok"/>
721+ </group>
722+ </form>
723+ </field>
724+ </record>
725+
726+ <record id="action_account_importer" model="ir.actions.act_window">
727+ <field name="name">Import Accounts from CSV</field>
728+ <field name="res_model">account_admin_tools.account_importer</field>
729+ <field name="view_type">form</field>
730+ <field name="view_mode">form</field>
731+ <field name="view_id" ref="view_account_importer_form"/>
732+ <field name="target">new</field>
733+ </record>
734+ <menuitem id="menu_action_account_importer"
735+ parent="menu_action_account_admin_tools_import"
736+ action="action_account_importer"
737+ sequence="10"/>
738+
739+ </data>
740+</openerp>
741
742=== added file 'account_admin_tools/account_importer_wizard.py'
743--- account_admin_tools/account_importer_wizard.py 1970-01-01 00:00:00 +0000
744+++ account_admin_tools/account_importer_wizard.py 2012-12-07 10:00:37 +0000
745@@ -0,0 +1,268 @@
746+# -*- coding: utf-8 -*-
747+##############################################################################
748+#
749+# OpenERP, Open Source Management Solution
750+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
751+# $Id$
752+#
753+# This program is free software: you can redistribute it and/or modify
754+# it under the terms of the GNU Affero General Public License as published
755+# by the Free Software Foundation, either version 3 of the License, or
756+# (at your option) any later version.
757+#
758+# This program is distributed in the hope that it will be useful,
759+# but WITHOUT ANY WARRANTY; without even the implied warranty of
760+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
761+# GNU Affero General Public License for more details.
762+#
763+# You should have received a copy of the GNU Affero General Public License
764+# along with this program. If not, see <http://www.gnu.org/licenses/>.
765+#
766+##############################################################################
767+"""
768+Account Importer
769+"""
770+__author__ = "Borja López Soilán (Pexego)"
771+
772+import time
773+import csv
774+import base64
775+import StringIO
776+import logging
777+import re
778+from osv import fields, osv
779+from tools.translate import _
780+
781+
782+class account_importer_wizard(osv.osv_memory):
783+ """
784+ Account Importer
785+
786+ Creates accounts from a CSV file.
787+
788+ The CSV file lines are expected to have at least the code and name of the
789+ account.
790+
791+ The wizard will find the account brothers (or parent) having the same
792+ account code sufix, and will autocomplete the rest of the account
793+ parameters (account type, reconcile, parent account...).
794+
795+ The CSV file lines are tested to be valid account lines using the regular
796+ expresion options of the wizard.
797+ """
798+ _name = "account_importer_wizard"
799+ _description = "Account importation wizard"
800+
801+ _columns = {
802+ #
803+ # Account move parameters
804+ #
805+ 'company_id': fields.many2one('res.company', 'Company', required=True),
806+ 'overwrite': fields.boolean('Overwrite',
807+ help="If the account already\ exists, overwrite its name?"),
808+ #
809+ # Input file
810+ #
811+ 'input_file': fields.binary('File', filters="*.csv", required=True),
812+ 'input_file_name': fields.char('File name', size=256),
813+ 'csv_delimiter': fields.char('Delimiter', size=1, required=True),
814+ 'csv_quotechar': fields.char('Quote', size=1, required=True),
815+ 'csv_code_index': fields.integer('Code field', required=True),
816+ 'csv_code_regexp': fields.char('Code regexp', size=32, required=True),
817+ 'csv_name_index': fields.integer('Name field', required=True),
818+ 'csv_name_regexp': fields.char('Name regexp', size=32, required=True),
819+
820+ }
821+
822+ _defaults = {
823+ 'company_id': lambda self, cr, uid, context:
824+ self.pool.get('res.users').browse(cr, uid,
825+ uid, context).company_id.id,
826+ 'csv_delimiter': lambda *a: ';',
827+ 'csv_quotechar': lambda *a: '"',
828+ 'csv_code_index': lambda *a: 0,
829+ 'csv_name_index': lambda *a: 1,
830+ 'csv_code_regexp': lambda *a: r'^[0-9]+$',
831+ 'csv_name_regexp': lambda *a: r'^.*$',
832+ }
833+
834+ def _find_parent_account_id(self, cr, uid, wiz, account_code, context=None):
835+ """
836+ Finds the parent account given an account code.
837+ It will remove the last digit of the code until it finds an account that
838+ matches exactly the code.
839+ """
840+ if len(account_code) > 0:
841+ parent_account_code = account_code[:-1]
842+ while len(parent_account_code) > 0:
843+ account_ids = self.pool.get('account.account').search(cr, uid, [
844+ ('code', '=', parent_account_code),
845+ ('company_id', '=', wiz.company_id.id)
846+ ])
847+ if account_ids and len(account_ids) > 0:
848+ return account_ids[0]
849+ parent_account_code = parent_account_code[:-1]
850+ # No parent found
851+ return None
852+
853+ def _find_brother_account_id(self, cr, uid, wiz, account_code, context=None):
854+ """
855+ Finds a brother account given an account code.
856+ It will remove the last digit of the code until it finds
857+ an account that matches the begin of the code.
858+ """
859+ if len(account_code) > 0:
860+ brother_account_code = account_code[:-1]
861+ while len(brother_account_code) > 0:
862+ account_ids = self.pool.get('account.account').search(cr,
863+ uid, [
864+ ('code', '=like', brother_account_code + '%%'),
865+ ('company_id',
866+ '=', wiz.company_id.id)
867+ ])
868+ if account_ids and len(account_ids) > 0:
869+ return account_ids[0]
870+ brother_account_code = brother_account_code[:-1]
871+ # No brother found
872+ return None
873+
874+ def action_import(self, cr, uid, ids, context=None):
875+ """
876+ Imports the accounts from the CSV file using the options from the
877+ wizard.
878+ """
879+ # List of the imported accounts
880+ imported_account_ids = []
881+
882+ logger = logging.getLogger("account_importer")
883+ for wiz in self.browse(cr, uid, ids, context):
884+ if not wiz.input_file:
885+ raise osv.except_osv(_('UserError'),
886+ _("You need to select a file!"))
887+
888+ # Decode the file data
889+ data = base64.b64decode(wiz.input_file)
890+
891+ #
892+ # Read the file
893+ #
894+ reader = csv.reader(StringIO.StringIO(data),
895+ delimiter=str(wiz.csv_delimiter),
896+ quotechar=str(wiz.csv_quotechar))
897+
898+ for record in reader:
899+ # Ignore short records
900+ if len(record) > wiz.csv_code_index \
901+ and len(record) > wiz.csv_name_index:
902+
903+ record_code = record[wiz.csv_code_index]
904+ record_name = record[wiz.csv_name_index]
905+
906+ #
907+ # Ignore invalid records
908+ #
909+ if re.match(wiz.csv_code_regexp, record_code) \
910+ and re.match(wiz.csv_name_regexp, record_name):
911+
912+ #
913+ # Search for the account
914+ #
915+ account_ids = self.pool.get(
916+ 'account.account').search(cr,
917+ uid, [
918+ ('code',
919+ '=', record_code),
920+ ('company_id',
921+ '=', wiz.company_id.id)
922+ ])
923+ if account_ids:
924+ if wiz.overwrite:
925+ logger.debug("Overwriting account: %s %s"
926+ % (record_code, record_name))
927+ self.pool.get('account.account').write(cr,
928+ uid, account_ids, {
929+ 'name': record_name
930+ })
931+ imported_account_ids.extend(account_ids)
932+ else:
933+ #
934+ # Find the account's parent
935+ #
936+ parent_account_id = self._find_parent_account_id(
937+ cr,
938+ uid, wiz, record_code)
939+
940+ if not parent_account_id:
941+ logger.warning("Couldn't find a parent\
942+ account for: %s" % record_code)
943+
944+ #
945+ # Find the account's brother (will be used as template)
946+ #
947+ brother_account_id = self._find_brother_account_id(cr,
948+ uid, wiz, record_code)
949+
950+ if not brother_account_id:
951+ logger.warning("Couldn't find a\
952+ brother account for: %s" % record_code)
953+
954+ brother_account = self.pool.get(
955+ 'account.account').browse(cr,
956+ uid, brother_account_id)
957+
958+ #
959+ # Create the new account
960+ #
961+ logger.debug("Creating new account:\
962+ %s %s" % (record_code, record_name))
963+ account_id = self.pool.get(
964+ 'account.account').create(cr,
965+ uid, {
966+ 'code': record_code,
967+ 'name': record_name,
968+ 'parent_id': parent_account_id,
969+ 'type': brother_account.type,
970+ 'user_type': brother_account.user_type.id,
971+ 'reconcile': brother_account.reconcile,
972+ 'company_id': wiz.company_id.id,
973+ 'currency_id': brother_account.currency_id.id,
974+ 'currency_mode': brother_account.currency_mode,
975+ 'check_history': brother_account.check_history,
976+ 'active': 1,
977+ 'tax_ids': [(6, 0, [tax.id for tax
978+ in brother_account.tax_ids])],
979+ 'note': False,
980+ })
981+
982+ imported_account_ids.append(account_id)
983+ else:
984+ logger.warning("Invalid record format \
985+ (ignoring line): %s" % repr(record))
986+ else:
987+ logger.warning("Too short record \
988+ (ignoring line): %s" % repr(record))
989+
990+ #
991+ # Show the accounts to the user
992+ #
993+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid,
994+ [
995+ ('model', '=', 'ir.ui.view'),
996+ ('module', '=', 'account'),
997+ ('name', '=', 'view_account_form')
998+ ])
999+ resource_id = self.pool.get('ir.model.data').read(cr, uid,
1000+ model_data_ids, fields=['res_id'])[0]['res_id']
1001+
1002+ return {
1003+ 'name': _("Imported accounts"),
1004+ 'type': 'ir.actions.act_window',
1005+ 'res_model': 'account.account',
1006+ 'view_type': 'form',
1007+ 'view_mode': 'tree, form',
1008+ 'views': [(False, 'tree'), (resource_id, 'form')],
1009+ 'domain': "[('id', 'in', %s)]" % imported_account_ids,
1010+ }
1011+
1012+
1013+account_importer_wizard()
1014
1015=== added file 'account_admin_tools/account_importer_wizard.xml'
1016--- account_admin_tools/account_importer_wizard.xml 1970-01-01 00:00:00 +0000
1017+++ account_admin_tools/account_importer_wizard.xml 2012-12-07 10:00:37 +0000
1018@@ -0,0 +1,62 @@
1019+<?xml version="1.0" encoding="utf-8"?>
1020+<openerp>
1021+ <data>
1022+
1023+ <record id="view_account_importer_wizard_form" model="ir.ui.view">
1024+ <field name="name">account_importer_wizard.form</field>
1025+ <field name="model">account_importer_wizard</field>
1026+ <field name="type">form</field>
1027+ <field name="arch" type="xml">
1028+ <form string="Account importer">
1029+ <label string="This wizard will import accounts from a CSV file." colspan="4"/>
1030+ <label string="Only the account code and name are needed, the rest of the required account data will be filled based on its brother accounts (same code begining)." colspan="4"/>
1031+ <label string=""/>
1032+ <newline/>
1033+ <group string="Account parameters" colspan="4">
1034+ <label string="Select the parameters for the account"/>
1035+ <group colspan="4">
1036+ <field name="company_id"/>
1037+ <field name="overwrite"/>
1038+ </group>
1039+ </group>
1040+ <group string="Input file" colspan="4">
1041+ <label string="Select the CSV file with the lines for the account move"/>
1042+ <group colspan="4">
1043+ <field name="input_file_name" string="File"/>
1044+ <field name="input_file" filename="input_file_name" nolabel="1"/>
1045+ <group colspan="2">
1046+ <separator string="File format" colspan="4"/>
1047+ <field name="csv_delimiter"/>
1048+ <field name="csv_quotechar"/>
1049+ </group>
1050+ <group colspan="2">
1051+ <separator string="Record format" colspan="4"/>
1052+ <field name="csv_code_index"/>
1053+ <field name="csv_code_regexp"/>
1054+ <field name="csv_name_index"/>
1055+ <field name="csv_name_regexp"/>
1056+ </group>
1057+ </group>
1058+ </group>
1059+ <group colspan="4">
1060+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
1061+ <button string="Import" name="action_import" type="object" icon="gtk-ok"/>
1062+ </group>
1063+ </form>
1064+ </field>
1065+ </record>
1066+
1067+ <record id="action_account_importer_wizard" model="ir.actions.act_window">
1068+ <field name="name">Import Accounts from CSV</field>
1069+ <field name="res_model">account_importer_wizard</field>
1070+ <field name="view_type">form</field>
1071+ <field name="view_mode">form</field>
1072+ <field name="target">new</field>
1073+ </record>
1074+ <menuitem id="menu_action_account_importer_wizard"
1075+ parent="menu_action_account_admin_tools"
1076+ action="action_account_importer_wizard"
1077+ sequence="10"/>
1078+
1079+ </data>
1080+</openerp>
1081
1082=== added file 'account_admin_tools/account_move_importer.py'
1083--- account_admin_tools/account_move_importer.py 1970-01-01 00:00:00 +0000
1084+++ account_admin_tools/account_move_importer.py 2012-12-07 10:00:37 +0000
1085@@ -0,0 +1,300 @@
1086+# -*- coding: utf-8 -*-
1087+##############################################################################
1088+#
1089+# OpenERP, Open Source Management Solution
1090+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
1091+# $Id$
1092+#
1093+# This program is free software: you can redistribute it and/or modify
1094+# it under the terms of the GNU Affero General Public License as published
1095+# by the Free Software Foundation, either version 3 of the License, or
1096+# (at your option) any later version.
1097+#
1098+# This program is distributed in the hope that it will be useful,
1099+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1100+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1101+# GNU Affero General Public License for more details.
1102+#
1103+# You should have received a copy of the GNU Affero General Public License
1104+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1105+#
1106+##############################################################################
1107+"""
1108+Account Move Importer
1109+"""
1110+__author__ = "Borja López Soilán (Pexego)"
1111+
1112+import time
1113+import logging
1114+import csv
1115+import base64
1116+import StringIO
1117+import re
1118+from osv import fields, osv
1119+from tools.translate import _
1120+
1121+
1122+class account_move_importer(osv.osv_memory):
1123+ """
1124+ Account Move Importer
1125+
1126+ Wizard that imports a CSV file into a new account move.
1127+
1128+ The CSV file is expected to have at least the account code, a reference
1129+ (description of the move line), the debit and the credit.
1130+
1131+ The lines of the CSV file are tested to be valid account move lines
1132+ using the regular expresions set on the wizard.
1133+ """
1134+ _name = "account_admin_tools.account_move_importer"
1135+ _description = "Account move importation wizard"
1136+
1137+ _columns = {
1138+ #
1139+ # Account move parameters
1140+ #
1141+ 'company_id': fields.many2one('res.company', 'Company',
1142+ required=True),
1143+ 'ref': fields.char('Ref', size=64, required=True),
1144+ 'period_id': fields.many2one('account.period', 'Period',
1145+ required=True),
1146+ 'journal_id': fields.many2one('account.journal', 'Journal',
1147+ required=True),
1148+ 'date': fields.date('Date', required=True),
1149+ 'type': fields.selection([
1150+ ('pay_voucher', 'Cash Payment'),
1151+ ('bank_pay_voucher', 'Bank Payment'),
1152+ ('rec_voucher', 'Cash Receipt'),
1153+ ('bank_rec_voucher', 'Bank Receipt'),
1154+ ('cont_voucher', 'Contra'),
1155+ ('journal_sale_vou', 'Journal Sale'),
1156+ ('journal_pur_voucher', 'Journal Purchase'),
1157+ ('journal_voucher', 'Journal Voucher'),
1158+ ], 'Type', select=True, required=True),
1159+ #
1160+ # Input file
1161+ #
1162+ 'input_file': fields.binary('File', filters="*.csv", required=True),
1163+ 'input_file_name': fields.char('File name', size=256),
1164+ 'csv_delimiter': fields.char('Delimiter', size=1, required=True),
1165+ 'csv_quotechar': fields.char('Quote', size=1, required=True),
1166+ 'csv_decimal_separator': fields.char('Decimal sep.', size=1,
1167+ required=True),
1168+ 'csv_thousands_separator': fields.char('Thousands sep.', size=1,
1169+ required=True),
1170+ 'csv_code_index': fields.integer('Code field', required=True),
1171+ 'csv_code_regexp': fields.char('Code regexp', size=32, required=True),
1172+ 'csv_ref_index': fields.integer('Ref field', required=True),
1173+ 'csv_ref_regexp': fields.char('Ref regexp', size=32, required=True),
1174+ 'csv_debit_index': fields.integer('Debit field', required=True),
1175+ 'csv_debit_regexp': fields.char('Debit regexp', size=32,
1176+ required=True),
1177+ 'csv_credit_index': fields.integer('Credit field', required=True),
1178+ 'csv_credit_regexp': fields.char('Credit regexp', size=32,
1179+ required=True),
1180+ }
1181+
1182+ def _get_default_period_id(self, cr, uid, context=None):
1183+ """
1184+ Returns the default period to use (based on account.move)
1185+ """
1186+ period_ids = self.pool.get('account.period').find(cr, uid)
1187+ return period_ids and period_ids[0] or False
1188+
1189+ _defaults = {
1190+ 'company_id': lambda self, cr, uid, context:
1191+ self.pool.get('res.users').browse(cr, uid, uid,
1192+ context).company_id.id,
1193+ 'period_id': _get_default_period_id,
1194+ 'date': lambda *a: time.strftime('%Y-%m-%d'),
1195+ 'type': lambda *a: 'journal_voucher', # Based on account move
1196+ 'csv_delimiter': lambda *a: ';',
1197+ 'csv_quotechar': lambda *a: '"',
1198+ 'csv_decimal_separator': lambda *a: '.',
1199+ 'csv_thousands_separator': lambda *a: ',',
1200+ 'csv_code_index': lambda *a: 0,
1201+ 'csv_ref_index': lambda *a: 1,
1202+ 'csv_debit_index': lambda *a: 2,
1203+ 'csv_credit_index': lambda *a: 3,
1204+ 'csv_code_regexp': lambda *a: r'^[0-9]+$',
1205+ 'csv_ref_regexp': lambda *a: r'^.*$',
1206+ 'csv_debit_regexp': lambda *a: r'^[0-9\-\.\,]*$',
1207+ 'csv_credit_regexp': lambda *a: r'^[0-9\-\.\,]*$',
1208+ }
1209+
1210+ def _get_accounts_map(self, cr, uid, context=None):
1211+ """
1212+ Find the receivable/payable accounts that are associated with
1213+ a single partner and return a (account.id, partner.id) map
1214+ """
1215+ partner_ids = self.pool.get('res.partner').search(cr, uid, [],
1216+ context=context)
1217+ accounts_map = {}
1218+ for partner in self.pool.get('res.partner').browse(cr, uid,
1219+ partner_ids, context=context):
1220+ #
1221+ # Add the receivable account to the map
1222+ #
1223+ if accounts_map.get(partner.property_account_receivable.id,
1224+ None) is None:
1225+ accounts_map[
1226+ partner.property_account_receivable.id] = partner.id
1227+ else:
1228+ # Two partners with the same receivable account: ignore
1229+ # this account!
1230+ accounts_map[partner.property_account_receivable.id] = 0
1231+ #
1232+ # Add the payable account to the map
1233+ #
1234+ if accounts_map.get(partner.property_account_payable.id,
1235+ None) is None:
1236+ accounts_map[partner.property_account_payable.id] = partner.id
1237+ else:
1238+ # Two partners with the same receivable account: ignore
1239+ # this account!
1240+ accounts_map[partner.property_account_payable.id] = 0
1241+ return accounts_map
1242+
1243+ def action_import(self, cr, uid, ids, context=None):
1244+ """
1245+ Imports a CSV file into a new account move using the options from
1246+ the wizard.
1247+ """
1248+ accounts_map = self._get_accounts_map(cr, uid, context=context)
1249+ logger = logging.getLogger("account_move_importer")
1250+
1251+ for wiz in self.browse(cr, uid, ids, context=context):
1252+ if not wiz.input_file:
1253+ raise osv.except_osv(_('UserError'),
1254+ _("You need to select a file!"))
1255+
1256+ account_move_data = self.pool.get('account.move').default_get(cr,
1257+ uid, ['state', 'name'])
1258+ account_move_data.update({
1259+ 'ref': wiz.ref,
1260+ 'journal_id': wiz.journal_id.id,
1261+ 'period_id': wiz.period_id.id,
1262+ 'date': wiz.date,
1263+ 'type': wiz.type,
1264+ 'line_id': [],
1265+ 'partner_id': False,
1266+ 'to_check': 0
1267+ })
1268+
1269+ lines_data = account_move_data['line_id']
1270+
1271+ # Decode the file data
1272+ data = base64.b64decode(wiz.input_file)
1273+
1274+ #
1275+ # Read the file
1276+ #
1277+ reader = csv.reader(StringIO.StringIO(data),
1278+ delimiter=str(wiz.csv_delimiter),
1279+ quotechar=str(wiz.csv_quotechar))
1280+
1281+ for record in reader:
1282+ # Ignore short records
1283+ if len(record) > wiz.csv_code_index \
1284+ and len(record) > wiz.csv_ref_index \
1285+ and len(record) > wiz.csv_debit_index \
1286+ and len(record) > wiz.csv_credit_index:
1287+
1288+ record_code = record[wiz.csv_code_index]
1289+ record_ref = record[wiz.csv_ref_index]
1290+ record_debit = record[wiz.csv_debit_index]
1291+ record_credit = record[wiz.csv_credit_index]
1292+
1293+ #
1294+ # Ignore invalid records
1295+ #
1296+ if re.match(wiz.csv_code_regexp, record_code) \
1297+ and re.match(wiz.csv_ref_regexp, record_ref) \
1298+ and re.match(wiz.csv_debit_regexp, record_debit) \
1299+ and re.match(wiz.csv_credit_regexp, record_credit):
1300+ #
1301+ # Clean the input amounts
1302+ #
1303+ record_debit = float(record_debit.replace(wiz.csv_thousands_separator, '').replace(wiz.csv_decimal_separator, '.'))
1304+ record_credit = float(record_credit.replace(wiz.csv_thousands_separator, '').replace(wiz.csv_decimal_separator, '.'))
1305+
1306+ #
1307+ # Find the account (or fail!)
1308+ #
1309+ account_ids = self.pool.get(
1310+ 'account.account').search(cr,
1311+ uid, [
1312+ ('code',
1313+ '=', record_code),
1314+ ('company_id',
1315+ '=', wiz.company_id.id)
1316+ ])
1317+ if not account_ids:
1318+ raise osv.except_osv(_('Error'), _("Account\
1319+ not found: %s!") % record_code)
1320+
1321+ #
1322+ # Prepare the line data
1323+ #
1324+ line_data = {
1325+ 'account_id': account_ids[0],
1326+ 'debit': 0.0,
1327+ 'credit': 0.0,
1328+ 'name': record_ref,
1329+ 'ref': False,
1330+ 'currency_id': False,
1331+ 'tax_amount': False,
1332+ 'partner_id': accounts_map.get(account_ids[0])
1333+ or False,
1334+ 'tax_code_id': False,
1335+ 'date_maturity': False,
1336+ 'amount_currency': False,
1337+ 'analytic_account_id': False,
1338+ }
1339+
1340+ #
1341+ # Create a debit line + a credit line if needed
1342+ #
1343+ line_data_debit = line_data.copy()
1344+ line_data_credit = line_data
1345+ if record_debit != 0.0:
1346+ line_data_debit['debit'] = record_debit
1347+ lines_data.append((0, 0, line_data_debit))
1348+ if record_credit != 0.0:
1349+ line_data_credit['credit'] = record_credit
1350+ lines_data.append((0, 0, line_data_credit))
1351+ else:
1352+ logger.warning("Invalid record format\
1353+ (ignoring line): %s" % repr(record))
1354+ else:
1355+ logger.warning("Too short record\
1356+ (ignoring line): %s" % repr(record))
1357+
1358+ # Finally create the move
1359+ move_id = self.pool.get('account.move').create(cr, uid,
1360+ account_move_data)
1361+
1362+ #
1363+ # Show the move to the user
1364+ #
1365+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
1366+ ('model', '=', 'ir.ui.view'),
1367+ ('module', '=', 'account'),
1368+ ('name', '=', 'view_move_form')
1369+ ])
1370+ resource_id = self.pool.get('ir.model.data').read(cr, uid,
1371+ model_data_ids, fields=['res_id'], context=context)[0]['res_id']
1372+
1373+ return {
1374+ 'name': _("Imported account moves"),
1375+ 'type': 'ir.actions.act_window',
1376+ 'res_model': 'account.move',
1377+ 'view_type': 'form',
1378+ 'view_mode': 'form, tree',
1379+ #'view_id': (resource_id, 'View'),
1380+ 'views': [(False, 'tree'), (resource_id, 'form')],
1381+ 'domain': "[('id', '=', %s)]" % move_id,
1382+ 'context': context,
1383+ }
1384+
1385+account_move_importer()
1386
1387=== added file 'account_admin_tools/account_move_importer.xml'
1388--- account_admin_tools/account_move_importer.xml 1970-01-01 00:00:00 +0000
1389+++ account_admin_tools/account_move_importer.xml 2012-12-07 10:00:37 +0000
1390@@ -0,0 +1,75 @@
1391+<?xml version="1.0" encoding="utf-8"?>
1392+<openerp>
1393+ <data>
1394+
1395+ <record id="view_account_move_importer_form" model="ir.ui.view">
1396+ <field name="name">account_move_importer.form</field>
1397+ <field name="model">account_admin_tools.account_move_importer</field>
1398+ <field name="type">form</field>
1399+ <field name="arch" type="xml">
1400+ <form string="Account move importer">
1401+ <label string="This wizard will import one account move from a CSV file." colspan="4"/>
1402+ <label string="Note: It will fail if any of the accounts do not exist in OpenERP." colspan="4"/>
1403+ <label string="" colspan="4"/>
1404+ <newline/>
1405+ <group string="Account move parameters" colspan="4">
1406+ <label string="Select the parameters for the account move"/>
1407+ <group colspan="4">
1408+ <field name="ref"/>
1409+ <field name="company_id"/>
1410+ <newline/>
1411+ <field name="journal_id"/>
1412+ <field name="type"/>
1413+ <newline/>
1414+ <field name="period_id"/>
1415+ <field name="date"/>
1416+ </group>
1417+ </group>
1418+ <group string="Input file" colspan="4">
1419+ <label string="Select the CSV file with the lines for the account move"/>
1420+ <group colspan="4">
1421+ <field name="input_file_name" string="File"/>
1422+ <field name="input_file" filename="input_file_name" nolabel="1"/>
1423+ <group colspan="2">
1424+ <separator string="File format" colspan="4"/>
1425+ <field name="csv_delimiter"/>
1426+ <field name="csv_quotechar"/>
1427+ <field name="csv_thousands_separator"/>
1428+ <field name="csv_decimal_separator"/>
1429+ </group>
1430+ <group colspan="2">
1431+ <separator string="Record format" colspan="4"/>
1432+ <field name="csv_code_index"/>
1433+ <field name="csv_code_regexp"/>
1434+ <field name="csv_ref_index"/>
1435+ <field name="csv_ref_regexp"/>
1436+ <field name="csv_debit_index"/>
1437+ <field name="csv_debit_regexp"/>
1438+ <field name="csv_credit_index"/>
1439+ <field name="csv_credit_regexp"/>
1440+ </group>
1441+ </group>
1442+ </group>
1443+ <group colspan="4">
1444+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
1445+ <button string="Import" name="action_import" type="object" icon="gtk-ok"/>
1446+ </group>
1447+ </form>
1448+ </field>
1449+ </record>
1450+
1451+ <record id="action_account_move_importer" model="ir.actions.act_window">
1452+ <field name="name">Import Account Move from CSV</field>
1453+ <field name="res_model">account_admin_tools.account_move_importer</field>
1454+ <field name="view_type">form</field>
1455+ <field name="view_mode">form</field>
1456+ <field name="view_id" ref="view_account_move_importer_form"/>
1457+ <field name="target">new</field>
1458+ </record>
1459+ <menuitem id="menu_action_account_move_importer"
1460+ parent="menu_action_account_admin_tools_import"
1461+ action="action_account_move_importer"
1462+ sequence="20"/>
1463+
1464+ </data>
1465+</openerp>
1466
1467=== added file 'account_admin_tools/account_move_importer_wizard.py'
1468--- account_admin_tools/account_move_importer_wizard.py 1970-01-01 00:00:00 +0000
1469+++ account_admin_tools/account_move_importer_wizard.py 2012-12-07 10:00:37 +0000
1470@@ -0,0 +1,266 @@
1471+# -*- coding: utf-8 -*-
1472+##############################################################################
1473+#
1474+# OpenERP, Open Source Management Solution
1475+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
1476+# $Id$
1477+#
1478+# This program is free software: you can redistribute it and/or modify
1479+# it under the terms of the GNU Affero General Public License as published
1480+# by the Free Software Foundation, either version 3 of the License, or
1481+# (at your option) any later version.
1482+#
1483+# This program is distributed in the hope that it will be useful,
1484+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1485+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1486+# GNU Affero General Public License for more details.
1487+#
1488+# You should have received a copy of the GNU Affero General Public License
1489+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1490+#
1491+##############################################################################
1492+"""
1493+Account Move Importer
1494+"""
1495+__author__ = "Borja López Soilán (Pexego)"
1496+
1497+import time
1498+import csv
1499+import base64
1500+import logging
1501+import StringIO
1502+import re
1503+from osv import fields, osv
1504+from tools.translate import _
1505+
1506+
1507+class account_move_importer_wizard(osv.osv_memory):
1508+ """
1509+ Account Move Importer
1510+
1511+ Wizard that imports a CSV file into a new account move.
1512+
1513+ The CSV file is expected to have at least the account code, a reference
1514+ (description of the move line), the debit and the credit.
1515+
1516+ The lines of the CSV file are tested to be valid account move lines
1517+ using the regular expresions set on the wizard.
1518+ """
1519+ _name = "account_move_importer_wizard"
1520+ _description = "Account move importation wizard"
1521+
1522+ _columns = {
1523+ #
1524+ # Account move parameters
1525+ #
1526+ 'company_id': fields.many2one('res.company', 'Company',
1527+ required=True),
1528+ 'ref': fields.char('Ref', size=64, required=True),
1529+ 'period_id': fields.many2one('account.period', 'Period',
1530+ required=True),
1531+ 'journal_id': fields.many2one('account.journal', 'Journal',
1532+ required=True),
1533+ 'date': fields.date('Date', required=True),
1534+
1535+ 'type': fields.selection([
1536+ ('pay_voucher', 'Cash Payment'),
1537+ ('bank_pay_voucher', 'Bank Payment'),
1538+ ('rec_voucher', 'Cash Receipt'),
1539+ ('bank_rec_voucher', 'Bank Receipt'),
1540+ ('cont_voucher', 'Contra'),
1541+ ('journal_sale_vou', 'Journal Sale'),
1542+ ('journal_pur_voucher', 'Journal Purchase'),
1543+ ('journal_voucher', 'Journal Voucher'),
1544+ ], 'Type', select=True, required=True),
1545+ #
1546+ # Input file
1547+ #
1548+ 'input_file': fields.binary('File', filters="*.csv",
1549+ required=True),
1550+ 'input_file_name': fields.char('File name', size=256),
1551+ 'csv_delimiter': fields.char('Delimiter', size=1,
1552+ required=True),
1553+ 'csv_quotechar': fields.char('Quote', size=1, required=True),
1554+ 'csv_decimal_separator': fields.char('Decimal sep.', size=1,
1555+ required=True),
1556+ 'csv_thousands_separator': fields.char('Thousands sep.', size=1,
1557+ required=True),
1558+ 'csv_code_index': fields.integer('Code field', required=True),
1559+ 'csv_code_regexp': fields.char('Code regexp', size=32, required=True),
1560+ 'csv_ref_index': fields.integer('Ref field', required=True),
1561+ 'csv_ref_regexp': fields.char('Ref regexp', size=32, required=True),
1562+ 'csv_debit_index': fields.integer('Debit field', required=True),
1563+ 'csv_debit_regexp': fields.char('Debit regexp', size=32,
1564+ required=True),
1565+ 'csv_credit_index': fields.integer('Credit field', required=True),
1566+ 'csv_credit_regexp': fields.char('Credit regexp', size=32,
1567+ required=True),
1568+ }
1569+
1570+ def _get_default_period_id(self, cr, uid, context=None):
1571+ """
1572+ Returns the default period to use (based on account.move)
1573+ """
1574+ period_ids = self.pool.get('account.period').find(cr, uid)
1575+ return period_ids and period_ids[0] or False
1576+
1577+ _defaults = {
1578+ 'company_id': lambda self, cr, uid, context:
1579+ self.pool.get('res.users').browse(cr, uid, uid,
1580+ context).company_id.id,
1581+ 'period_id': _get_default_period_id,
1582+ 'date': lambda *a: time.strftime('%Y-%m-%d'),
1583+ 'type': lambda *a: 'journal_voucher', # Based on account move
1584+ 'csv_delimiter': lambda *a: ';',
1585+ 'csv_quotechar': lambda *a: '"',
1586+ 'csv_decimal_separator': lambda *a: '.',
1587+ 'csv_thousands_separator': lambda *a: ',',
1588+ 'csv_code_index': lambda *a: 0,
1589+ 'csv_ref_index': lambda *a: 1,
1590+ 'csv_debit_index': lambda *a: 2,
1591+ 'csv_credit_index': lambda *a: 3,
1592+ 'csv_code_regexp': lambda *a: r'^[0-9]+$',
1593+ 'csv_ref_regexp': lambda *a: r'^.*$',
1594+ 'csv_debit_regexp': lambda *a: r'^[0-9\-\.\,]*$',
1595+ 'csv_credit_regexp': lambda *a: r'^[0-9\-\.\,]*$',
1596+ }
1597+
1598+ def action_import(self, cr, uid, ids, context=None):
1599+ """
1600+ Imports a CSV file into a new account move using the options from
1601+ the wizard.
1602+ """
1603+ logger = logging.getLogger("account_move_importer")
1604+ for wiz in self.browse(cr, uid, ids, context):
1605+ if not wiz.input_file:
1606+ raise osv.except_osv(_('UserError'),
1607+ _("You need to select a file!"))
1608+
1609+ account_move_data = self.pool.get('account.move').default_get(cr,
1610+ uid, ['state', 'name'])
1611+ account_move_data.update({
1612+ 'ref': wiz.ref,
1613+ 'journal_id': wiz.journal_id.id,
1614+ 'period_id': wiz.period_id.id,
1615+ 'date': wiz.date,
1616+ 'type': wiz.type,
1617+ 'line_id': [],
1618+ 'partner_id': False,
1619+ 'to_check': 0
1620+ })
1621+
1622+ lines_data = account_move_data['line_id']
1623+
1624+ # Decode the file data
1625+ data = base64.b64decode(wiz.input_file)
1626+
1627+ #
1628+ # Read the file
1629+ #
1630+ reader = csv.reader(StringIO.StringIO(data),
1631+ delimiter=str(wiz.csv_delimiter),
1632+ quotechar=str(wiz.csv_quotechar))
1633+
1634+ for record in reader:
1635+ # Ignore short records
1636+ if len(record) > wiz.csv_code_index \
1637+ and len(record) > wiz.csv_ref_index \
1638+ and len(record) > wiz.csv_debit_index \
1639+ and len(record) > wiz.csv_credit_index:
1640+
1641+ record_code = record[wiz.csv_code_index]
1642+ record_ref = record[wiz.csv_ref_index]
1643+ record_debit = record[wiz.csv_debit_index]
1644+ record_credit = record[wiz.csv_credit_index]
1645+
1646+ #
1647+ # Ignore invalid records
1648+ #
1649+ if re.match(wiz.csv_code_regexp, record_code) \
1650+ and re.match(wiz.csv_ref_regexp, record_ref) \
1651+ and re.match(wiz.csv_debit_regexp, record_debit) \
1652+ and re.match(wiz.csv_credit_regexp, record_credit):
1653+ #
1654+ # Clean the input amounts
1655+ #
1656+ record_debit = float(record_debit.replace(wiz.csv_thousands_separator, '').replace(wiz.csv_decimal_separator, '.'))
1657+ record_credit = float(record_credit.replace(wiz.csv_thousands_separator, '').replace(wiz.csv_decimal_separator, '.'))
1658+
1659+ #
1660+ # Find the account (or fail!)
1661+ #
1662+ account_ids = self.pool.get(
1663+ 'account.account').search(cr,
1664+ uid, [
1665+ ('code',
1666+ '=', record_code),
1667+ ('company_id',
1668+ '=', wiz.company_id.id)
1669+ ])
1670+ if not account_ids:
1671+ raise osv.except_osv(_('Error'),
1672+ _("Account not found: %s!") % record_code)
1673+
1674+ #
1675+ # Prepare the line data
1676+ #
1677+ line_data = {
1678+ 'account_id': account_ids[0],
1679+ 'debit': 0.0,
1680+ 'credit': 0.0,
1681+ 'name': record_ref,
1682+ 'ref': False,
1683+ 'currency_id': False,
1684+ 'tax_amount': False,
1685+ 'partner_id': False,
1686+ 'tax_code_id': False,
1687+ 'date_maturity': False,
1688+ 'amount_currency': False,
1689+ 'analytic_account_id': False,
1690+ }
1691+
1692+ #
1693+ # Create a debit line + a credit line if needed
1694+ #
1695+ line_data_debit = line_data.copy()
1696+ line_data_credit = line_data
1697+ if record_debit != 0.0:
1698+ line_data_debit['debit'] = record_debit
1699+ lines_data.append((0, 0, line_data_debit))
1700+ if record_credit != 0.0:
1701+ line_data_credit['credit'] = record_credit
1702+ lines_data.append((0, 0, line_data_credit))
1703+ else:
1704+ logger.warning("Invalid record format\
1705+ (ignoring line): %s" % repr(record))
1706+ else:
1707+ logger.warning("Too short record\
1708+ (ignoring line): %s" % repr(record))
1709+
1710+ # Finally create the move
1711+ move_id = self.pool.get('account.move').create(cr, uid,
1712+ account_move_data)
1713+
1714+ #
1715+ # Show the move to the user
1716+ #
1717+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
1718+ ('model', '=', 'ir.ui.view'),
1719+ ('module', '=', 'account'),
1720+ ('name', '=', 'view_move_form')
1721+ ])
1722+ resource_id = self.pool.get('ir.model.data').read(cr, uid,
1723+ model_data_ids, fields=['res_id'])[0]['res_id']
1724+
1725+ return {
1726+ 'name': _("Imported account moves"),
1727+ 'type': 'ir.actions.act_window',
1728+ 'res_model': 'account.move',
1729+ 'view_type': 'form',
1730+ 'view_mode': 'form,tree',
1731+ #'view_id': (resource_id, 'View'),
1732+ 'views': [(False, 'tree'), (resource_id, 'form')],
1733+ 'domain': "[('id', '=', %s)]" % move_id,
1734+ }
1735+
1736+account_move_importer_wizard()
1737
1738=== added file 'account_admin_tools/account_move_importer_wizard.xml'
1739--- account_admin_tools/account_move_importer_wizard.xml 1970-01-01 00:00:00 +0000
1740+++ account_admin_tools/account_move_importer_wizard.xml 2012-12-07 10:00:37 +0000
1741@@ -0,0 +1,74 @@
1742+<?xml version="1.0" encoding="utf-8"?>
1743+<openerp>
1744+ <data>
1745+
1746+ <record id="view_account_move_importer_wizard_form" model="ir.ui.view">
1747+ <field name="name">account_move_importer_wizard.form</field>
1748+ <field name="model">account_move_importer_wizard</field>
1749+ <field name="type">form</field>
1750+ <field name="arch" type="xml">
1751+ <form string="Account move importer">
1752+ <label string="This wizard will import one account move from a CSV file." colspan="4"/>
1753+ <label string="Note: It will fail if any of the accounts do not exist in OpenERP." colspan="4"/>
1754+ <label string=""/>
1755+ <newline/>
1756+ <group string="Account move parameters" colspan="4">
1757+ <label string="Select the parameters for the account move"/>
1758+ <group colspan="4">
1759+ <field name="ref"/>
1760+ <field name="company_id"/>
1761+ <newline/>
1762+ <field name="journal_id"/>
1763+ <field name="type"/>
1764+ <newline/>
1765+ <field name="period_id"/>
1766+ <field name="date"/>
1767+ </group>
1768+ </group>
1769+ <group string="Input file" colspan="4">
1770+ <label string="Select the CSV file with the lines for the account move"/>
1771+ <group colspan="4">
1772+ <field name="input_file_name" string="File"/>
1773+ <field name="input_file" filename="input_file_name" nolabel="1"/>
1774+ <group colspan="2">
1775+ <separator string="File format" colspan="4"/>
1776+ <field name="csv_delimiter"/>
1777+ <field name="csv_quotechar"/>
1778+ <field name="csv_thousands_separator"/>
1779+ <field name="csv_decimal_separator"/>
1780+ </group>
1781+ <group colspan="2">
1782+ <separator string="Record format" colspan="4"/>
1783+ <field name="csv_code_index"/>
1784+ <field name="csv_code_regexp"/>
1785+ <field name="csv_ref_index"/>
1786+ <field name="csv_ref_regexp"/>
1787+ <field name="csv_debit_index"/>
1788+ <field name="csv_debit_regexp"/>
1789+ <field name="csv_credit_index"/>
1790+ <field name="csv_credit_regexp"/>
1791+ </group>
1792+ </group>
1793+ </group>
1794+ <group colspan="4">
1795+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
1796+ <button string="Import" name="action_import" type="object" icon="gtk-ok"/>
1797+ </group>
1798+ </form>
1799+ </field>
1800+ </record>
1801+
1802+ <record id="action_account_move_importer_wizard" model="ir.actions.act_window">
1803+ <field name="name">Import Account Move from CSV</field>
1804+ <field name="res_model">account_move_importer_wizard</field>
1805+ <field name="view_type">form</field>
1806+ <field name="view_mode">form</field>
1807+ <field name="target">new</field>
1808+ </record>
1809+ <menuitem id="menu_action_account_move_importer_wizard"
1810+ parent="menu_action_account_admin_tools"
1811+ action="action_account_move_importer_wizard"
1812+ sequence="20"/>
1813+
1814+ </data>
1815+</openerp>
1816
1817=== added file 'account_admin_tools/admin_tools_menu.xml'
1818--- account_admin_tools/admin_tools_menu.xml 1970-01-01 00:00:00 +0000
1819+++ account_admin_tools/admin_tools_menu.xml 2012-12-07 10:00:37 +0000
1820@@ -0,0 +1,20 @@
1821+<?xml version="1.0" encoding="utf-8"?>
1822+<openerp>
1823+ <data>
1824+
1825+ <menuitem id="menu_action_account_admin_tools"
1826+ name="Admin Tools"
1827+ parent="account.menu_finance"
1828+ sequence="999"/>
1829+
1830+ <menuitem id="menu_action_account_admin_tools_import"
1831+ name="Import"
1832+ parent="menu_action_account_admin_tools"
1833+ sequence="10"/>
1834+
1835+ <menuitem id="menu_action_account_admin_tools_repair"
1836+ name="Check and Repair"
1837+ parent="menu_action_account_admin_tools"
1838+ sequence="20"/>
1839+ </data>
1840+</openerp>
1841
1842=== added directory 'account_admin_tools/i18n'
1843=== added file 'account_admin_tools/move_partner_account.py'
1844--- account_admin_tools/move_partner_account.py 1970-01-01 00:00:00 +0000
1845+++ account_admin_tools/move_partner_account.py 2012-12-07 10:00:37 +0000
1846@@ -0,0 +1,188 @@
1847+# -*- coding: utf-8 -*-
1848+##############################################################################
1849+#
1850+# OpenERP, Open Source Management Solution
1851+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
1852+# $Id$
1853+#
1854+# This program is free software: you can redistribute it and/or modify
1855+# it under the terms of the GNU Affero General Public License as published
1856+# by the Free Software Foundation, either version 3 of the License, or
1857+# (at your option) any later version.
1858+#
1859+# This program is distributed in the hope that it will be useful,
1860+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1861+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1862+# GNU Affero General Public License for more details.
1863+#
1864+# You should have received a copy of the GNU Affero General Public License
1865+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1866+#
1867+##############################################################################
1868+"""
1869+Move Partner Account Wizard
1870+
1871+Checks that the account moves use the partner account instead of a
1872+generic account.
1873+"""
1874+__author__ = "Borja López Soilán (Pexego)"
1875+
1876+import re
1877+from osv import fields, osv
1878+from tools.translate import _
1879+
1880+
1881+class move_partner_account(osv.osv_memory):
1882+ """
1883+ Move Partner Account Wizard
1884+
1885+ Checks that the account moves use the partner account instead of a
1886+ generic account.
1887+ """
1888+ _name = "account_admin_tools.move_partner_account"
1889+ _description = "Move Partner Account Wizard"
1890+
1891+ _columns = {
1892+ 'state': fields.selection([('new', 'New'), ('done', 'Done')],
1893+ 'Status', readonly=True),
1894+ #
1895+ # Account move parameters
1896+ #
1897+ 'company_id': fields.many2one('res.company', 'Company',
1898+ required=True, readonly=True),
1899+ 'period_ids': fields.many2many('account.period',
1900+ 'move_partner_account_period_rel', 'wizard_id',
1901+ 'period_id', "Periods"),
1902+ 'account_payable_id': fields.many2one('account.account',
1903+ 'Account Payable', required=True),
1904+ 'account_receivable_id': fields.many2one('account.account',
1905+ 'Account Receivable', required=True),
1906+ }
1907+
1908+ def _get_payable_account_id(self, cr, uid, context=None):
1909+ """
1910+ Gets the default payable (supplier) account property value for the
1911+ current (user's) company.
1912+ """
1913+ if context is None:
1914+ context = {}
1915+ company_id = self.pool.get('res.users').browse(cr, uid, uid,
1916+ context).company_id.id
1917+ res = None
1918+ property_ids = self.pool.get('ir.property').search(cr, uid, [
1919+ '|',
1920+ ('company_id', '=', company_id),
1921+ ('company_id', '=', False),
1922+ ('name', '=', 'property_account_payable'),
1923+ ('res_id', '=', False)
1924+ ])
1925+ if property_ids:
1926+ property = self.pool.get(
1927+ 'ir.property').browse(cr, uid, property_ids[0])
1928+ if property:
1929+ try:
1930+ # OpenERP 5.0 and 5.2/6.0 revno <= 2236
1931+ res = int(property.value.split(',')[1])
1932+ except AttributeError:
1933+ # OpenERP 6.0 revno >= 2236
1934+ res = property.value_reference.id
1935+ return res
1936+
1937+ def _get_receivable_account_id(self, cr, uid, context=None):
1938+ """
1939+ Gets the default receivable (customer) account property value for the
1940+ current (user's) company.
1941+ """
1942+ company_id = self.pool.get('res.users').browse(cr, uid, uid,
1943+ context).company_id.id
1944+ res = None
1945+ property_ids = self.pool.get('ir.property').search(cr, uid, [
1946+ '|',
1947+ ('company_id', '=', company_id),
1948+ ('company_id', '=', False),
1949+ ('name', '=', 'property_account_receivable'),
1950+ ('res_id', '=', False)
1951+ ])
1952+ if property_ids:
1953+ property = self.pool.get('ir.property').browse(cr, uid,
1954+ property_ids[0])
1955+ if property:
1956+ try:
1957+ # OpenERP 5.0 and 5.2/6.0 revno <= 2236
1958+ res = int(property.value.split(',')[1])
1959+ except AttributeError:
1960+ # OpenERP 6.0 revno >= 2236
1961+ res = property.value_reference.id
1962+ return res
1963+
1964+ _defaults = {
1965+ 'company_id': lambda self, cr, uid, context:
1966+ self.pool.get('res.users').browse(cr, uid, uid,
1967+ context).company_id.id,
1968+ 'account_payable_id': _get_payable_account_id,
1969+ 'account_receivable_id': _get_receivable_account_id,
1970+ }
1971+
1972+ def action_set_partner_accounts_in_moves(self, cr, uid, ids, context=None):
1973+ """
1974+ Action that searchs for the account moves that do not use the partner
1975+ account, but the generic payable/receivable one, and sets the partner
1976+ account instead.
1977+ """
1978+ for wiz in self.browse(cr, uid, ids, context):
1979+ period_ids = [period.id for period in wiz.period_ids]
1980+ query_acc = """
1981+ UPDATE account_move_line
1982+ SET account_id=%s
1983+ WHERE partner_id=%s
1984+ AND account_id=%s
1985+ """
1986+ query_inv = """
1987+ UPDATE account_invoice
1988+ SET account_id=%s
1989+ WHERE partner_id=%s
1990+ AND account_id=%s
1991+ """
1992+ periods_str = ','.join(map(str, period_ids))
1993+ if period_ids:
1994+ query_acc += """ AND period_id IN (%s)""" % periods_str
1995+ query_inv += """ AND period_id IN (%s)""" % periods_str
1996+
1997+ partner_ids = self.pool.get('res.partner').search(cr, uid, [])
1998+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids):
1999+ # Receivable account
2000+ if partner.property_account_receivable.id != wiz.account_receivable_id.id:
2001+ cr.execute(query_acc % (partner.property_account_receivable.id, partner.id, wiz.account_receivable_id.id))
2002+ cr.execute(query_inv % (partner.property_account_receivable.id, partner.id, wiz.account_receivable_id.id))
2003+ # Payable account
2004+ if partner.property_account_payable.id != wiz.account_payable_id.id:
2005+ cr.execute(query_acc % (partner.property_account_payable.id, partner.id, wiz.account_payable_id.id))
2006+ cr.execute(query_inv % (partner.property_account_payable.id, partner.id, wiz.account_payable_id.id))
2007+
2008+ # Update the wizard status
2009+ self.write(cr, uid, [wiz.id], {'state': 'done'})
2010+
2011+ #
2012+ # Return the next view: Show 'done' view
2013+ #
2014+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
2015+ ('model', '=', 'ir.ui.view'),
2016+ ('module', '=', 'account_admin_tools'),
2017+ ('name', '=', 'view_move_partner_account_done_form')
2018+ ])
2019+ resource_id = self.pool.get('ir.model.data').read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
2020+
2021+ return {
2022+ 'name': _("Set Partner Accounts in Moves"),
2023+ 'type': 'ir.actions.act_window',
2024+ 'res_model': 'account_admin_tools.move_partner_account',
2025+ 'view_type': 'form',
2026+ 'view_mode': 'form',
2027+ 'views': [(resource_id, 'form')],
2028+ 'domain': "[('id', 'in', %s)]" % ids,
2029+ 'context': context,
2030+ 'target': 'new',
2031+ }
2032+
2033+
2034+move_partner_account()
2035
2036=== added file 'account_admin_tools/move_partner_account.xml'
2037--- account_admin_tools/move_partner_account.xml 1970-01-01 00:00:00 +0000
2038+++ account_admin_tools/move_partner_account.xml 2012-12-07 10:00:37 +0000
2039@@ -0,0 +1,58 @@
2040+<?xml version="1.0" encoding="utf-8"?>
2041+<openerp>
2042+ <data>
2043+ <record id="view_move_partner_account_form" model="ir.ui.view">
2044+ <field name="name">move_partner_account.form</field>
2045+ <field name="model">account_admin_tools.move_partner_account</field>
2046+ <field name="type">form</field>
2047+ <field name="arch" type="xml">
2048+ <form string="Set Partner Accounts in Moves">
2049+ <label string="This wizard will set the receivable/payable account of the partners, in moves and invoices where a generic receivable/payable account was used instead." colspan="4"/>
2050+ <label string="" colspan="4"/>
2051+ <newline/>
2052+ <group string="Parameters" colspan="4">
2053+ <separator string="Accounts to replace with partner accounts" colspan="4"/>
2054+ <field name="account_receivable_id"/>
2055+ <field name="account_payable_id"/>
2056+ <newline/>
2057+ <separator string="Filter for the moves to update" colspan="4"/>
2058+ <field name="period_ids" colspan="4"/>
2059+ </group>
2060+ <group colspan="4">
2061+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2062+ <button string="Set partner accounts" name="action_set_partner_accounts_in_moves" type="object" icon="gtk-apply"/>
2063+ </group>
2064+ </form>
2065+ </field>
2066+ </record>
2067+
2068+ <record id="view_move_partner_account_done_form" model="ir.ui.view">
2069+ <field name="name">move_partner_account.done.form</field>
2070+ <field name="model">account_admin_tools.move_partner_account</field>
2071+ <field name="type">form</field>
2072+ <field name="arch" type="xml">
2073+ <form string="Set Partner Accounts in Moves">
2074+ <label string="The partner's receivable/payable accounts have been set succesfuly on the account moves!" colspan="4"/>
2075+ <label string="" colspan="4"/>
2076+ <group colspan="4">
2077+ <button string="Done" special="cancel" icon="gtk-ok"/>
2078+ </group>
2079+ </form>
2080+ </field>
2081+ </record>
2082+
2083+ <record id="action_move_partner_account" model="ir.actions.act_window">
2084+ <field name="name">Set Partner Accounts in Moves</field>
2085+ <field name="res_model">account_admin_tools.move_partner_account</field>
2086+ <field name="view_type">form</field>
2087+ <field name="view_mode">form</field>
2088+ <field name="view_id" ref="view_move_partner_account_form"/>
2089+ <field name="target">new</field>
2090+ </record>
2091+ <menuitem id="menu_action_move_partner_account"
2092+ parent="menu_action_account_admin_tools_repair"
2093+ action="action_move_partner_account"
2094+ sequence="110"/>
2095+
2096+ </data>
2097+</openerp>
2098
2099=== added file 'account_admin_tools/move_partner_account_wizard.py'
2100--- account_admin_tools/move_partner_account_wizard.py 1970-01-01 00:00:00 +0000
2101+++ account_admin_tools/move_partner_account_wizard.py 2012-12-07 10:00:37 +0000
2102@@ -0,0 +1,145 @@
2103+# -*- coding: utf-8 -*-
2104+##############################################################################
2105+#
2106+# OpenERP, Open Source Management Solution
2107+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
2108+# $Id$
2109+#
2110+# This program is free software: you can redistribute it and/or modify
2111+# it under the terms of the GNU Affero General Public License as published
2112+# by the Free Software Foundation, either version 3 of the License, or
2113+# (at your option) any later version.
2114+#
2115+# This program is distributed in the hope that it will be useful,
2116+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2117+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2118+# GNU Affero General Public License for more details.
2119+#
2120+# You should have received a copy of the GNU Affero General Public License
2121+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2122+#
2123+##############################################################################
2124+"""
2125+Move Partner Account Wizard
2126+
2127+Checks that the account moves use the partner account instead of a
2128+generic account.
2129+"""
2130+__author__ = "Borja López Soilán (Pexego)"
2131+
2132+import re
2133+from osv import fields, osv
2134+from tools.translate import _
2135+
2136+
2137+class move_partner_account_wizard(osv.osv_memory):
2138+ """
2139+ Move Partner Account Wizard
2140+
2141+ Checks that the account moves use the partner account instead of a
2142+ generic account.
2143+ """
2144+ _name = "move_partner_account_wizard"
2145+ _description = "Move Partner Account Wizard"
2146+
2147+ _columns = {
2148+ #
2149+ # Account move parameters
2150+ #
2151+ 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True),
2152+
2153+ 'period_ids': fields.many2many('account.period', 'move_partner_account_wizard_period_rel', 'wizard_id', 'period_id', "Periods"),
2154+
2155+ 'account_payable_id': fields.many2one('account.account', 'Account Payable', required=True),
2156+ 'account_receivable_id': fields.many2one('account.account', 'Account Receivable', required=True),
2157+ }
2158+
2159+ def _get_payable_account_id(self, cr, uid, context=None):
2160+ if context is None:
2161+ context = {}
2162+ company_id = self.pool.get(
2163+ 'res.users').browse(cr, uid, uid, context).company_id.id
2164+ res = None
2165+ property_ids = self.pool.get('ir.property').search(cr, uid, [
2166+ '|',
2167+ ('company_id', '=', company_id),
2168+ ('company_id', '=', False),
2169+ ('name', '=', 'property_account_payable'),
2170+ ('res_id', '=', False)
2171+ ])
2172+ if property_ids:
2173+ property = self.pool.get(
2174+ 'ir.property').browse(cr, uid, property_ids[0])
2175+ if property:
2176+ try:
2177+ # OpenERP 5.0 and 5.2/6.0 revno <= 2236
2178+ res = int(property.value.split(',')[1])
2179+ except AttributeError:
2180+ # OpenERP 6.0 revno >= 2236
2181+ res = property.value_reference.id
2182+ return res
2183+
2184+ def _get_receivable_account_id(self, cr, uid, context=None):
2185+ company_id = self.pool.get(
2186+ 'res.users').browse(cr, uid, uid, context).company_id.id
2187+ res = None
2188+ property_ids = self.pool.get('ir.property').search(cr, uid, [
2189+ '|',
2190+ ('company_id', '=', company_id),
2191+ ('company_id', '=', False),
2192+ ('name', '=', 'property_account_receivable'),
2193+ ('res_id', '=', False)
2194+ ])
2195+ if property_ids:
2196+ property = self.pool.get(
2197+ 'ir.property').browse(cr, uid, property_ids[0])
2198+ if property:
2199+ try:
2200+ # OpenERP 5.0 and 5.2/6.0 revno <= 2236
2201+ res = int(property.value.split(',')[1])
2202+ except AttributeError:
2203+ # OpenERP 6.0 revno >= 2236
2204+ res = property.value_reference.id
2205+ return res
2206+
2207+ _defaults = {
2208+ 'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
2209+ 'account_payable_id': _get_payable_account_id,
2210+ 'account_receivable_id': _get_receivable_account_id,
2211+ }
2212+
2213+ def action_set_partner_accounts_in_moves(self, cr, uid, ids, context=None):
2214+ for wiz in self.browse(cr, uid, ids, context):
2215+ period_ids = [period.id for period in wiz.period_ids]
2216+ query_acc = """
2217+ UPDATE account_move_line
2218+ SET account_id=%s
2219+ WHERE partner_id=%s
2220+ AND account_id=%s
2221+ """
2222+ query_inv = """
2223+ UPDATE account_invoice
2224+ SET account_id=%s
2225+ WHERE partner_id=%s
2226+ AND account_id=%s
2227+ """
2228+ periods_str = ','.join(map(str, period_ids))
2229+ if period_ids:
2230+ query_acc += """ AND period_id IN (%s)""" % periods_str
2231+ query_inv += """ AND period_id IN (%s)""" % periods_str
2232+
2233+ partner_ids = self.pool.get('res.partner').search(cr, uid, [])
2234+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids):
2235+ # Receivable account
2236+ if partner.property_account_receivable.id != wiz.account_receivable_id.id:
2237+ cr.execute(query_acc % (partner.property_account_receivable.id, partner.id, wiz.account_receivable_id.id))
2238+ cr.execute(query_inv % (partner.property_account_receivable.id, partner.id, wiz.account_receivable_id.id))
2239+ # Payable account
2240+ if partner.property_account_payable.id != wiz.account_payable_id.id:
2241+ cr.execute(query_acc % (partner.property_account_payable.id, partner.id, wiz.account_payable_id.id))
2242+ cr.execute(query_inv % (partner.property_account_payable.id, partner.id, wiz.account_payable_id.id))
2243+
2244+ return {}
2245+
2246+
2247+move_partner_account_wizard()
2248
2249=== added file 'account_admin_tools/move_partner_account_wizard.xml'
2250--- account_admin_tools/move_partner_account_wizard.xml 1970-01-01 00:00:00 +0000
2251+++ account_admin_tools/move_partner_account_wizard.xml 2012-12-07 10:00:37 +0000
2252@@ -0,0 +1,42 @@
2253+<?xml version="1.0" encoding="utf-8"?>
2254+<openerp>
2255+ <data>
2256+ <record id="view_move_partner_account_wizard_form" model="ir.ui.view">
2257+ <field name="name">move_partner_account_wizard.form</field>
2258+ <field name="model">move_partner_account_wizard</field>
2259+ <field name="type">form</field>
2260+ <field name="arch" type="xml">
2261+ <form string="Set partner account in moves">
2262+ <label string="This wizard will set the receivable/payable account of the partners, in moves and invoices where a generic receivable/payable account was used instead." colspan="4"/>
2263+ <label string=""/>
2264+ <newline/>
2265+ <group string="Parameters" colspan="4">
2266+ <separator string="Accounts to replace with partner accounts" colspan="4"/>
2267+ <field name="account_receivable_id"/>
2268+ <field name="account_payable_id"/>
2269+ <newline/>
2270+ <separator string="Filter for the moves to update" colspan="4"/>
2271+ <field name="period_ids" colspan="4"/>
2272+ </group>
2273+ <group colspan="4">
2274+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2275+ <button string="Set partner accounts" name="action_set_partner_accounts_in_moves" type="object" icon="gtk-ok"/>
2276+ </group>
2277+ </form>
2278+ </field>
2279+ </record>
2280+
2281+ <record id="action_move_partner_account_wizard" model="ir.actions.act_window">
2282+ <field name="name">Set Partner Account in Moves</field>
2283+ <field name="res_model">move_partner_account_wizard</field>
2284+ <field name="view_type">form</field>
2285+ <field name="view_mode">form</field>
2286+ <field name="target">new</field>
2287+ </record>
2288+ <menuitem id="menu_action_move_partner_account_wizard"
2289+ parent="menu_action_account_admin_tools"
2290+ action="action_move_partner_account_wizard"
2291+ sequence="20"/>
2292+
2293+ </data>
2294+</openerp>
2295
2296=== added file 'account_admin_tools/revalidate_moves.py'
2297--- account_admin_tools/revalidate_moves.py 1970-01-01 00:00:00 +0000
2298+++ account_admin_tools/revalidate_moves.py 2012-12-07 10:00:37 +0000
2299@@ -0,0 +1,157 @@
2300+# -*- coding: utf-8 -*-
2301+##############################################################################
2302+#
2303+# OpenERP, Open Source Management Solution
2304+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
2305+# $Id$
2306+#
2307+# This program is free software: you can redistribute it and/or modify
2308+# it under the terms of the GNU Affero General Public License as published
2309+# by the Free Software Foundation, either version 3 of the License, or
2310+# (at your option) any later version.
2311+#
2312+# This program is distributed in the hope that it will be useful,
2313+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2314+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2315+# GNU Affero General Public License for more details.
2316+#
2317+# You should have received a copy of the GNU Affero General Public License
2318+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2319+#
2320+##############################################################################
2321+"""
2322+Revalidate Account Moves Wizard
2323+"""
2324+__author__ = "Borja López Soilán (Pexego)"
2325+
2326+import re
2327+from osv import fields, osv
2328+from tools.translate import _
2329+
2330+
2331+class revalidate_moves(osv.osv_memory):
2332+ """
2333+ Revalidate Account Moves Wizard
2334+
2335+ Revalidates all the (already confirmed) moves, so their analytic lines
2336+ are recomputed (to fix the data after problems like this:
2337+ https://bugs.launchpad.net/openobject-addons/+bug/582988).
2338+ """
2339+ _name = "account_admin_tools.revalidate_moves"
2340+ _description = "Revalidate Account Moves Wizard"
2341+
2342+ _columns = {
2343+ 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True),
2344+ 'period_ids': fields.many2many('account.period', 'revalidate_moves_period_rel', 'wizard_id', 'period_id', "Periods"),
2345+ 'move_ids': fields.many2many('account.move', 'revalidate_moves_moves_rel', 'wizard_id', 'move_id', 'Moves'),
2346+ 'state': fields.selection([('new', 'New'), ('ready', 'Ready'), ('done', 'Done')], 'Status', readonly=True),
2347+ }
2348+
2349+ _defaults = {
2350+ 'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
2351+ 'move_ids': lambda self, cr, uid, context: context and context.get('move_ids', None),
2352+ 'period_ids': lambda self, cr, uid, context: context and context.get('period_ids', None),
2353+ 'state': lambda self, cr, uid, context: context and context.get('state', 'new'),
2354+ }
2355+
2356+ def _next_view(self, cr, uid, ids, view_name, args=None, context=None):
2357+ """
2358+ Return the next view
2359+ """
2360+ if context is None:
2361+ context = {}
2362+ if args is None:
2363+ args = {}
2364+ ctx = context.copy()
2365+ ctx.update(args)
2366+
2367+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
2368+ ('model', '=', 'ir.ui.view'),
2369+ ('module', '=', 'account_admin_tools'),
2370+ ('name', '=', view_name)
2371+ ])
2372+ resource_id = self.pool.get('ir.model.data').read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
2373+ return {
2374+ 'name': _("Revalidate Moves"),
2375+ 'type': 'ir.actions.act_window',
2376+ 'res_model': 'account_admin_tools.revalidate_moves',
2377+ 'view_type': 'form',
2378+ 'view_mode': 'form',
2379+ 'views': [(resource_id, 'form')],
2380+ 'domain': "[('id', 'in', %s)]" % ids,
2381+ 'context': ctx,
2382+ 'target': 'new',
2383+ }
2384+
2385+ def action_skip_new(self, cr, uid, ids, context=None):
2386+ """
2387+ Action that just skips the to the ready state
2388+ """
2389+ return self._next_view(cr, uid, ids, 'view_revalidate_moves_ready_form', {'state': 'ready'}, context)
2390+
2391+ def action_find_moves_missing_analytic_lines(self, cr, uid, ids, context=None):
2392+ """
2393+ Finds account moves with missing analytic lines and adds them
2394+ to the move_ids many2many field.
2395+ """
2396+ wiz = self.browse(cr, uid, ids[0], context)
2397+
2398+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
2399+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
2400+ if wiz.period_ids:
2401+ period_ids = [period.id for period in wiz.period_ids]
2402+ else:
2403+ period_ids = context and context.get('period_ids')
2404+
2405+ query = """
2406+ SELECT account_move_line.move_id FROM account_move_line
2407+ LEFT JOIN account_analytic_line
2408+ ON account_analytic_line.move_id = account_move_line.id
2409+ WHERE account_move_line.analytic_account_id IS NOT NULL AND account_analytic_line.id IS NULL
2410+ """
2411+ if period_ids:
2412+ query += """ AND period_id IN %s"""
2413+ cr.execute(query, (tuple(period_ids),))
2414+ else:
2415+ cr.execute(query)
2416+
2417+ move_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
2418+
2419+ #
2420+ # Return the next view: Show 'ready' view
2421+ #
2422+ args = {
2423+ 'move_ids': move_ids,
2424+ 'state': 'ready',
2425+ }
2426+ return self._next_view(cr, uid, ids, 'view_revalidate_moves_ready_form', args, context)
2427+
2428+ def action_revalidate_moves(self, cr, uid, ids, context=None):
2429+ """
2430+ Calls the validate method of the account moves for each move in the
2431+ move_ids many2many.
2432+ """
2433+ wiz = self.browse(cr, uid, ids[0], context)
2434+
2435+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
2436+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
2437+ if wiz.move_ids:
2438+ move_ids = [line.id for line in wiz.move_ids]
2439+ else:
2440+ move_ids = context and context.get('move_ids')
2441+
2442+ for move in self.pool.get('account.move').browse(cr, uid, move_ids, context=context):
2443+ # We validate the moves one by one to prevent problems
2444+ self.pool.get('account.move').validate(cr, uid, [move.id], context)
2445+
2446+ #
2447+ # Return the next view: Show 'done' view
2448+ #
2449+ args = {
2450+ 'move_ids': move_ids,
2451+ 'state': 'done',
2452+ }
2453+ return self._next_view(cr, uid, ids, 'view_revalidate_moves_done_form', args, context)
2454+
2455+
2456+revalidate_moves()
2457
2458=== added file 'account_admin_tools/revalidate_moves.xml'
2459--- account_admin_tools/revalidate_moves.xml 1970-01-01 00:00:00 +0000
2460+++ account_admin_tools/revalidate_moves.xml 2012-12-07 10:00:37 +0000
2461@@ -0,0 +1,82 @@
2462+<?xml version="1.0" encoding="utf-8"?>
2463+<openerp>
2464+ <data>
2465+ <record id="view_revalidate_moves_form" model="ir.ui.view">
2466+ <field name="name">revalidate_moves.form</field>
2467+ <field name="model">account_admin_tools.revalidate_moves</field>
2468+ <field name="type">form</field>
2469+ <field name="arch" type="xml">
2470+ <form string="Revalidate Account Moves">
2471+ <label string="This wizard will revalidate the account moves, so their analytic lines are regenerated." colspan="4"/>
2472+ <label string="" colspan="4"/>
2473+ <group colspan="4">
2474+ <separator string="Find moves with missing analytic lines" colspan="4"/>
2475+ <label string="You may now search for account moves with missing analytic lines on the given periods, or you may skip this step and select the moves by hand." colspan="4"/>
2476+ <label string="" colspan="4"/>
2477+ <newline/>
2478+ <field name="period_ids" colspan="4"/>
2479+ <label string="" colspan="4"/>
2480+ <newline/>
2481+ <button string="Search for moves" name="action_find_moves_missing_analytic_lines" type="object" icon="gtk-ok" colspan="4"/>
2482+ </group>
2483+ <label string="" colspan="4"/>
2484+ <group colspan="4">
2485+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2486+ <button string="Skip" name="action_skip_new" type="object" icon="gtk-go-forward"/>
2487+ </group>
2488+ </form>
2489+ </field>
2490+ </record>
2491+
2492+ <record id="view_revalidate_moves_ready_form" model="ir.ui.view">
2493+ <field name="name">revalidate_moves.ready.form</field>
2494+ <field name="model">account_admin_tools.revalidate_moves</field>
2495+ <field name="type">form</field>
2496+ <field name="arch" type="xml">
2497+ <form string="Revalidate Account Moves">
2498+ <label string="The selected moves will be revalidated, that will regenerate their analytic lines." colspan="4"/>
2499+ <label string="" colspan="4"/>
2500+ <separator string="Account moves to revalidate" colspan="4"/>
2501+ <field name="move_ids" colspan="4" nolabel="1"/>
2502+ <label string="" colspan="4"/>
2503+ <group colspan="4">
2504+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2505+ <button string="Revalidate selected moves" name="action_revalidate_moves" type="object" icon="gtk-apply"/>
2506+ </group>
2507+ </form>
2508+ </field>
2509+ </record>
2510+
2511+ <record id="view_revalidate_moves_done_form" model="ir.ui.view">
2512+ <field name="name">revalidate_moves.done.form</field>
2513+ <field name="model">account_admin_tools.revalidate_moves</field>
2514+ <field name="type">form</field>
2515+ <field name="arch" type="xml">
2516+ <form string="Revalidate Account Moves">
2517+ <label string="The moves have been revalidated sucessfully!" colspan="4"/>
2518+ <label string="" colspan="4"/>
2519+ <separator string="Revalidated account moves" colspan="4"/>
2520+ <field name="move_ids" colspan="4" nolabel="1" readonly="1"/>
2521+ <label string="" colspan="4"/>
2522+ <group colspan="4">
2523+ <button string="Done" special="cancel" icon="gtk-ok"/>
2524+ </group>
2525+ </form>
2526+ </field>
2527+ </record>
2528+
2529+ <record id="action_revalidate_moves" model="ir.actions.act_window">
2530+ <field name="name">Revalidate Account Moves (Regenerate Analytic Lines)</field>
2531+ <field name="res_model">account_admin_tools.revalidate_moves</field>
2532+ <field name="view_type">form</field>
2533+ <field name="view_mode">form</field>
2534+ <field name="view_id" ref="view_revalidate_moves_form"/>
2535+ <field name="target">new</field>
2536+ </record>
2537+ <menuitem id="menu_action_revalidate_moves"
2538+ parent="menu_action_account_admin_tools_repair"
2539+ action="action_revalidate_moves"
2540+ sequence="20"/>
2541+
2542+ </data>
2543+</openerp>
2544
2545=== added file 'account_admin_tools/revalidate_moves_wizard.py'
2546--- account_admin_tools/revalidate_moves_wizard.py 1970-01-01 00:00:00 +0000
2547+++ account_admin_tools/revalidate_moves_wizard.py 2012-12-07 10:00:37 +0000
2548@@ -0,0 +1,100 @@
2549+# -*- coding: utf-8 -*-
2550+##############################################################################
2551+#
2552+# OpenERP, Open Source Management Solution
2553+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
2554+# $Id$
2555+#
2556+# This program is free software: you can redistribute it and/or modify
2557+# it under the terms of the GNU Affero General Public License as published
2558+# by the Free Software Foundation, either version 3 of the License, or
2559+# (at your option) any later version.
2560+#
2561+# This program is distributed in the hope that it will be useful,
2562+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2563+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2564+# GNU Affero General Public License for more details.
2565+#
2566+# You should have received a copy of the GNU Affero General Public License
2567+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2568+#
2569+##############################################################################
2570+"""
2571+Revalidate Account Moves Wizard
2572+"""
2573+__author__ = "Borja López Soilán (Pexego)"
2574+
2575+import re
2576+from osv import fields, osv
2577+from tools.translate import _
2578+
2579+
2580+class revalidate_moves_wizard(osv.osv_memory):
2581+ """
2582+ Revalidate Account Moves Wizard
2583+
2584+ Revalidates all the (already confirmed) moves, so their analytic lines
2585+ are recomputed (to fix the data after problems like this:
2586+ https://bugs.launchpad.net/openobject-addons/+bug/582988).
2587+ """
2588+ _name = "revalidate_moves_wizard"
2589+ _description = "Revalidate Account Moves Wizard"
2590+
2591+ _columns = {
2592+ 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True),
2593+ 'period_ids': fields.many2many('account.period', 'revalidate_moves_wizard_period_rel', 'wizard_id', 'period_id', "Periods"),
2594+ 'move_ids': fields.many2many('account.move', 'revalidate_moves_wizard_moves_rel', 'wizard_id', 'move_id', 'Moves'),
2595+ 'state': fields.selection([('new', 'New'), ('ready', 'Ready'), ('done', 'Done')], 'Status', readonly=True),
2596+ }
2597+
2598+ _defaults = {
2599+ 'state': lambda *a: 'new',
2600+ }
2601+
2602+ def action_skip_new(self, cr, uid, ids, context=None):
2603+ """
2604+ Skips to the ready state.
2605+ """
2606+ for wiz in self.browse(cr, uid, ids, context):
2607+ self.write(cr, uid, [wiz.id], {'state': 'ready'})
2608+ return True
2609+
2610+ def action_find_moves_missing_analytic_lines(self, cr, uid, ids, context=None):
2611+ """
2612+ Finds account moves with missing analytic lines and adds them
2613+ to the move_ids many2many field.
2614+ """
2615+ for wiz in self.browse(cr, uid, ids, context):
2616+ period_ids = [period.id for period in wiz.period_ids]
2617+ query = """
2618+ SELECT account_move_line.move_id FROM account_move_line
2619+ LEFT JOIN account_analytic_line
2620+ ON account_analytic_line.move_id = account_move_line.id
2621+ WHERE account_move_line.analytic_account_id IS NOT NULL AND account_analytic_line.id IS NULL
2622+ """
2623+ periods_str = ','.join(map(str, period_ids))
2624+ if period_ids:
2625+ query += """ AND period_id IN (%s)""" % periods_str
2626+
2627+ cr.execute(query)
2628+ move_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
2629+ self.write(cr, uid, [wiz.id], {
2630+ 'move_ids': [(6, 0, move_ids)],
2631+ 'state': 'ready'
2632+ })
2633+ return True
2634+
2635+ def action_revalidate_moves(self, cr, uid, ids, context=None):
2636+ """
2637+ Calls the validate method of the account moves for each move in the
2638+ move_ids many2many.
2639+ """
2640+ for wiz in self.browse(cr, uid, ids, context):
2641+ for move in wiz.move_ids:
2642+ # We validate the moves one by one to prevent problems
2643+ self.pool.get(
2644+ 'account.move').validate(cr, uid, [move.id], context)
2645+ self.write(cr, uid, [wiz.id], {'state': 'done'})
2646+ return True
2647+
2648+revalidate_moves_wizard()
2649
2650=== added file 'account_admin_tools/revalidate_moves_wizard.xml'
2651--- account_admin_tools/revalidate_moves_wizard.xml 1970-01-01 00:00:00 +0000
2652+++ account_admin_tools/revalidate_moves_wizard.xml 2012-12-07 10:00:37 +0000
2653@@ -0,0 +1,45 @@
2654+<?xml version="1.0" encoding="utf-8"?>
2655+<openerp>
2656+ <data>
2657+ <record id="view_revalidate_moves_wizard_form" model="ir.ui.view">
2658+ <field name="name">revalidate_moves_wizard.form</field>
2659+ <field name="model">revalidate_moves_wizard</field>
2660+ <field name="type">form</field>
2661+ <field name="arch" type="xml">
2662+ <form string="Revalidate account moves">
2663+ <label string="This wizard will revalidate the given account moves, so their analytic lines are regenerated." colspan="4"/>
2664+ <label string=""/>
2665+ <newline/>
2666+ <group string="Moves to revalidate" attrs="{'invisible': [('state','!=','ready')]}">
2667+ <field name="move_ids" colspan="4" nolabel="1"/>
2668+ </group>
2669+ <group string="Find moves with missing analytic lines" colspan="4" attrs="{'invisible': [('state','!=','new')]}">
2670+ <button string="Search for moves" name="action_find_moves_missing_analytic_lines" type="object" icon="gtk-ok" states="new" colspan="4"/>
2671+ <separator string="Filter for the moves" colspan="4"/>
2672+ <field name="period_ids" colspan="4"/>
2673+ </group>
2674+ <group colspan="4">
2675+ <button string="Cancel" special="cancel" icon="gtk-cancel" states="new,ready"/>
2676+ <button string="Skip" name="action_skip_new" type="object" icon="gtk-go-forward" states="new"/>
2677+ <button string="Revalidate selected moves" name="action_revalidate_moves" type="object" icon="gtk-apply" states="ready"/>
2678+ <button string="Done" special="cancel" icon="gtk-ok" states="done"/>
2679+ </group>
2680+ <field name="state" invisible="1"/>
2681+ </form>
2682+ </field>
2683+ </record>
2684+
2685+ <record id="action_revalidate_moves_wizard" model="ir.actions.act_window">
2686+ <field name="name">Revalidate Account Moves (Regenerate Analytic Lines)</field>
2687+ <field name="res_model">revalidate_moves_wizard</field>
2688+ <field name="view_type">form</field>
2689+ <field name="view_mode">form</field>
2690+ <field name="target">new</field>
2691+ </record>
2692+ <menuitem id="menu_action_revalidate_moves_wizard"
2693+ parent="menu_action_account_admin_tools"
2694+ action="action_revalidate_moves_wizard"
2695+ sequence="50"/>
2696+
2697+ </data>
2698+</openerp>
2699
2700=== added file 'account_admin_tools/set_invoice_ref_in_moves.py'
2701--- account_admin_tools/set_invoice_ref_in_moves.py 1970-01-01 00:00:00 +0000
2702+++ account_admin_tools/set_invoice_ref_in_moves.py 2012-12-07 10:00:37 +0000
2703@@ -0,0 +1,205 @@
2704+# -*- coding: utf-8 -*-
2705+##############################################################################
2706+#
2707+# OpenERP, Open Source Management Solution
2708+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
2709+# $Id$
2710+#
2711+# This program is free software: you can redistribute it and/or modify
2712+# it under the terms of the GNU Affero General Public License as published
2713+# by the Free Software Foundation, either version 3 of the License, or
2714+# (at your option) any later version.
2715+#
2716+# This program is distributed in the hope that it will be useful,
2717+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2718+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2719+# GNU Affero General Public License for more details.
2720+#
2721+# You should have received a copy of the GNU Affero General Public License
2722+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2723+#
2724+##############################################################################
2725+"""
2726+Set Invoice Reference in Moves
2727+"""
2728+__author__ = "Borja López Soilán (Pexego)"
2729+
2730+import re
2731+from osv import fields, osv
2732+from tools.translate import _
2733+
2734+
2735+class set_invoice_ref_in_moves(osv.osv_memory):
2736+ """
2737+ Set Invoice Reference in Moves
2738+
2739+ Searchs for account moves associated with invoices that do not have the
2740+ right reference (the reference from a supplier invoice or the number from
2741+ a customer invoice) and lets the user fix them.
2742+ """
2743+ _name = "account_admin_tools.set_invoice_ref_in_moves"
2744+ _description = "Set Invoice Reference in Moves"
2745+
2746+ _columns = {
2747+ 'state': fields.selection([('new', 'New'), ('ready', 'Ready'), ('done', 'Done')], 'Status', readonly=True),
2748+ 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True),
2749+ 'period_ids': fields.many2many('account.period', 'set_invoice_ref_in_moves_period_rel', 'wizard_id', 'period_id', "Periods"),
2750+ 'move_ids': fields.many2many('account.move', 'set_invoice_ref_in_move_move_rel', 'wizard_id', 'move_id', 'Moves'),
2751+ }
2752+
2753+ _defaults = {
2754+ 'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
2755+ 'move_ids': lambda self, cr, uid, context: context and context.get('move_ids', None),
2756+ 'period_ids': lambda self, cr, uid, context: context and context.get('period_ids', None),
2757+ 'state': lambda self, cr, uid, context: context and context.get('state', 'new'),
2758+ }
2759+
2760+ def _next_view(self, cr, uid, ids, view_name, args=None, context=None):
2761+ """
2762+ Return the next wizard view
2763+ """
2764+ if context is None:
2765+ context = {}
2766+ if args is None:
2767+ args = {}
2768+ ctx = context.copy()
2769+ ctx.update(args)
2770+
2771+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
2772+ ('model', '=', 'ir.ui.view'),
2773+ ('module', '=', 'account_admin_tools'),
2774+ ('name', '=', view_name)
2775+ ])
2776+ resource_id = self.pool.get('ir.model.data').read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
2777+ return {
2778+ 'name': _("Set Invoice Reference in Moves"),
2779+ 'type': 'ir.actions.act_window',
2780+ 'res_model': 'account_admin_tools.set_invoice_ref_in_moves',
2781+ 'view_type': 'form',
2782+ 'view_mode': 'form',
2783+ 'views': [(resource_id, 'form')],
2784+ 'domain': "[('id', 'in', %s)]" % ids,
2785+ 'context': ctx,
2786+ 'target': 'new',
2787+ }
2788+
2789+ def _get_reference(self, cr, uid, invoice, context=None):
2790+ """
2791+ Get's the reference for an account move given the related invoice.
2792+ """
2793+ if invoice.type in ('in_invoice', 'in_refund'):
2794+ return invoice.reference
2795+ else:
2796+ return self.pool.get('account.invoice')._convert_ref(cr, uid, invoice.number)
2797+
2798+ def _is_valid_reference(self, cr, uid, reference, invoice, context=None):
2799+ """
2800+ Checks that the given reference matches the invoice reference or number.
2801+ """
2802+ if reference == invoice.reference:
2803+ return True
2804+ elif reference == self.pool.get('account.invoice')._convert_ref(cr, uid, invoice.number):
2805+ return True
2806+ else:
2807+ return False
2808+
2809+ def action_skip_new(self, cr, uid, ids, context=None):
2810+ """
2811+ Action that just skips the to the ready state
2812+ """
2813+ return self._next_view(cr, uid, ids, 'view_set_invoice_ref_in_moves_ready_form', {'state': 'ready'}, context)
2814+
2815+ def action_find_moves_with_wrong_invoice_ref(self, cr, uid, ids, context=None):
2816+ """
2817+ Action that searchs for account moves associated with invoices,
2818+ that do not have the right reference.
2819+ """
2820+ wiz = self.browse(cr, uid, ids[0], context)
2821+
2822+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
2823+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
2824+ if wiz.period_ids:
2825+ period_ids = [period.id for period in wiz.period_ids]
2826+ else:
2827+ period_ids = context and context.get('period_ids')
2828+
2829+ #
2830+ # Find the invoices (on the given periods)
2831+ #
2832+ args = []
2833+ if period_ids:
2834+ args = [('period_id', 'in', period_ids)]
2835+ invoice_ids = self.pool.get(
2836+ 'account.invoice').search(cr, uid, args, context=context)
2837+
2838+ #
2839+ # Get the moves with references not matching the desired ones
2840+ #
2841+ move_ids = []
2842+ for invoice in self.pool.get('account.invoice').browse(cr, uid, invoice_ids, context=context):
2843+ if invoice.move_id:
2844+ if not self._is_valid_reference(cr, uid, invoice.move_id.ref, invoice, context=context):
2845+ reference = self._get_reference(
2846+ cr, uid, invoice, context=context)
2847+ if reference and len(reference):
2848+ move_ids.append(invoice.move_id.id)
2849+
2850+ #
2851+ # Return the next view: Show 'ready' view
2852+ #
2853+ args = {
2854+ 'move_ids': move_ids,
2855+ 'state': 'ready',
2856+ }
2857+ return self._next_view(cr, uid, ids, 'view_set_invoice_ref_in_moves_ready_form', args, context)
2858+
2859+ def action_set_invoice_ref_in_moves(self, cr, uid, ids, context=None):
2860+ """
2861+ Action that sets the invoice reference or number as the account move
2862+ reference for the selected moves.
2863+ """
2864+ wiz = self.browse(cr, uid, ids[0], context)
2865+
2866+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
2867+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
2868+ if wiz.move_ids:
2869+ move_ids = [line.id for line in wiz.move_ids]
2870+ else:
2871+ move_ids = context and context.get('move_ids')
2872+
2873+ #
2874+ # Find the invoices of the moves
2875+ #
2876+ args = [('move_id', 'in', move_ids)]
2877+ invoice_ids = self.pool.get(
2878+ 'account.invoice').search(cr, uid, args, context=context)
2879+
2880+ #
2881+ # Update the moves with the reference of the invoice.
2882+ #
2883+ for invoice in self.pool.get('account.invoice').browse(cr, uid, invoice_ids, context=context):
2884+ if invoice.move_id:
2885+ reference = self._get_reference(
2886+ cr, uid, invoice, context=context)
2887+ if invoice.move_id.ref != reference:
2888+ self.pool.get('account.move').write(cr, uid, [invoice.move_id.id], {'ref': reference}, context=context)
2889+
2890+ #
2891+ # Update the move line references too
2892+ # (if they where equal to the move reference)
2893+ #
2894+ for line in invoice.move_id.line_id:
2895+ if line.ref == invoice.move_id.ref:
2896+ self.pool.get('account.move.line').write(cr, uid, [line.id], {'ref': reference}, context=context)
2897+
2898+ #
2899+ # Return the next view: Show 'done' view
2900+ #
2901+ args = {
2902+ 'move_ids': move_ids,
2903+ 'state': 'done',
2904+ }
2905+ return self._next_view(cr, uid, ids, 'view_set_invoice_ref_in_moves_done_form', args, context)
2906+
2907+
2908+set_invoice_ref_in_moves()
2909
2910=== added file 'account_admin_tools/set_invoice_ref_in_moves.xml'
2911--- account_admin_tools/set_invoice_ref_in_moves.xml 1970-01-01 00:00:00 +0000
2912+++ account_admin_tools/set_invoice_ref_in_moves.xml 2012-12-07 10:00:37 +0000
2913@@ -0,0 +1,82 @@
2914+<?xml version="1.0" encoding="utf-8"?>
2915+<openerp>
2916+ <data>
2917+ <record id="view_set_invoice_ref_in_moves_form" model="ir.ui.view">
2918+ <field name="name">set_invoice_ref_in_moves.form</field>
2919+ <field name="model">account_admin_tools.set_invoice_ref_in_moves</field>
2920+ <field name="type">form</field>
2921+ <field name="arch" type="xml">
2922+ <form string="Set Invoice Reference in Moves">
2923+ <label string="This wizard will set the reference in account moves associated with invoices, that don't match the invoice reference/number." colspan="4"/>
2924+ <label string="" colspan="4"/>
2925+ <group colspan="4">
2926+ <separator string="Find moves not matching the invoice reference/number" colspan="4"/>
2927+ <label string="You may now search for account moves, on the given periods, whose reference does not match the invoice reference or number; or you may skip this step and select the moves by hand." colspan="4"/>
2928+ <label string="" colspan="4"/>
2929+ <newline/>
2930+ <field name="period_ids" colspan="4"/>
2931+ <label string="" colspan="4"/>
2932+ <newline/>
2933+ <button string="Search for moves" name="action_find_moves_with_wrong_invoice_ref" type="object" icon="gtk-ok" colspan="4"/>
2934+ </group>
2935+ <label string="" colspan="4"/>
2936+ <group colspan="4">
2937+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2938+ <button string="Skip" name="action_skip_new" type="object" icon="gtk-go-forward"/>
2939+ </group>
2940+ </form>
2941+ </field>
2942+ </record>
2943+
2944+ <record id="view_set_invoice_ref_in_moves_ready_form" model="ir.ui.view">
2945+ <field name="name">set_invoice_ref_in_moves.ready.form</field>
2946+ <field name="model">account_admin_tools.set_invoice_ref_in_moves</field>
2947+ <field name="type">form</field>
2948+ <field name="arch" type="xml">
2949+ <form string="Set Invoice Reference in Moves">
2950+ <label string="The reference will be set, for the selected account moves, to the reference/number of the (supplier/customer) invoice." colspan="4"/>
2951+ <label string="" colspan="4"/>
2952+ <separator string="Account moves to update" colspan="4"/>
2953+ <field name="move_ids" colspan="4" nolabel="1"/>
2954+ <label string="" colspan="4"/>
2955+ <group colspan="4">
2956+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
2957+ <button string="Set invoice reference in moves" name="action_set_invoice_ref_in_moves" type="object" icon="gtk-apply"/>
2958+ </group>
2959+ </form>
2960+ </field>
2961+ </record>
2962+
2963+ <record id="view_set_invoice_ref_in_moves_done_form" model="ir.ui.view">
2964+ <field name="name">set_invoice_ref_in_moves.done.form</field>
2965+ <field name="model">account_admin_tools.set_invoice_ref_in_moves</field>
2966+ <field name="type">form</field>
2967+ <field name="arch" type="xml">
2968+ <form string="Set Invoice Reference in Moves">
2969+ <label string="The invoice references have been succesfully set on the account moves!" colspan="4"/>
2970+ <label string="" colspan="4"/>
2971+ <separator string="Updated account moves" colspan="4"/>
2972+ <field name="move_ids" colspan="4" nolabel="1" readonly="1"/>
2973+ <label string="" colspan="4"/>
2974+ <group colspan="4">
2975+ <button string="Done" special="cancel" icon="gtk-ok"/>
2976+ </group>
2977+ </form>
2978+ </field>
2979+ </record>
2980+
2981+ <record id="action_set_invoice_ref_in_moves" model="ir.actions.act_window">
2982+ <field name="name">Set Invoice Reference in Moves</field>
2983+ <field name="res_model">account_admin_tools.set_invoice_ref_in_moves</field>
2984+ <field name="view_type">form</field>
2985+ <field name="view_mode">form</field>
2986+ <field name="view_id" ref="view_set_invoice_ref_in_moves_form"/>
2987+ <field name="target">new</field>
2988+ </record>
2989+ <menuitem id="menu_action_set_invoice_ref_in_moves"
2990+ parent="menu_action_account_admin_tools_repair"
2991+ action="action_set_invoice_ref_in_moves"
2992+ sequence="130"/>
2993+
2994+ </data>
2995+</openerp>
2996
2997=== added file 'account_admin_tools/set_partner_in_moves.py'
2998--- account_admin_tools/set_partner_in_moves.py 1970-01-01 00:00:00 +0000
2999+++ account_admin_tools/set_partner_in_moves.py 2012-12-07 10:00:37 +0000
3000@@ -0,0 +1,214 @@
3001+# -*- coding: utf-8 -*-
3002+##############################################################################
3003+#
3004+# OpenERP, Open Source Management Solution
3005+# Copyright (C) 2004-2009 Pexego Sistemas Informáticos. All Rights Reserved
3006+# $Id$
3007+#
3008+# This program is free software: you can redistribute it and/or modify
3009+# it under the terms of the GNU Affero General Public License as published
3010+# by the Free Software Foundation, either version 3 of the License, or
3011+# (at your option) any later version.
3012+#
3013+# This program is distributed in the hope that it will be useful,
3014+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3015+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3016+# GNU Affero General Public License for more details.
3017+#
3018+# You should have received a copy of the GNU Affero General Public License
3019+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3020+#
3021+##############################################################################
3022+"""
3023+Set Partner in Moves Wizard
3024+"""
3025+__author__ = "Borja López Soilán (Pexego)"
3026+
3027+import re
3028+from osv import fields, osv
3029+from tools.translate import _
3030+
3031+
3032+class set_partner_in_moves(osv.osv_memory):
3033+ """
3034+ Set Partner in Moves Wizard
3035+
3036+ Searchs for account move lines of that use the payable/receivable account
3037+ of a single partner, and have no partner reference in the line,
3038+ and sets the partner reference (partner_id).
3039+ This may fix cases where the receivable/payable amounts displayed in the
3040+ partner form does not match the balance of the receivable/payable accounts.
3041+ """
3042+ _name = "account_admin_tools.set_partner_in_moves"
3043+ _description = "Set Partner in Moves Wizard"
3044+
3045+ _columns = {
3046+ 'state': fields.selection([('new', 'New'), ('ready', 'Ready'), ('done', 'Done')], 'Status', readonly=True),
3047+ 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True),
3048+ 'period_ids': fields.many2many('account.period', 'set_partner_in_moves_period_rel', 'wizard_id', 'period_id', "Periods"),
3049+ 'move_line_ids': fields.many2many('account.move.line', 'set_partner_in_move_move_line_rel', 'wizard_id', 'line_id', 'Move Lines'),
3050+ }
3051+
3052+ _defaults = {
3053+ 'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
3054+ 'move_line_ids': lambda self, cr, uid, context: context and context.get('move_line_ids', None),
3055+ 'period_ids': lambda self, cr, uid, context: context and context.get('period_ids', None),
3056+ 'state': lambda self, cr, uid, context: context and context.get('state', 'new'),
3057+ }
3058+
3059+ def _next_view(self, cr, uid, ids, view_name, args=None, context=None):
3060+ """
3061+ Return the next wizard view
3062+ """
3063+ if context is None:
3064+ context = {}
3065+ if args is None:
3066+ args = {}
3067+ ctx = context.copy()
3068+ ctx.update(args)
3069+
3070+ model_data_ids = self.pool.get('ir.model.data').search(cr, uid, [
3071+ ('model', '=', 'ir.ui.view'),
3072+ ('module', '=', 'account_admin_tools'),
3073+ ('name', '=', view_name)
3074+ ])
3075+ resource_id = self.pool.get('ir.model.data').read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
3076+ return {
3077+ 'name': _("Set Partner Reference in Moves"),
3078+ 'type': 'ir.actions.act_window',
3079+ 'res_model': 'account_admin_tools.set_partner_in_moves',
3080+ 'view_type': 'form',
3081+ 'view_mode': 'form',
3082+ 'views': [(resource_id, 'form')],
3083+ 'domain': "[('id', 'in', %s)]" % ids,
3084+ 'context': ctx,
3085+ 'target': 'new',
3086+ }
3087+
3088+ def _get_accounts_map(self, cr, uid, context=None):
3089+ """
3090+ Find the receivable/payable accounts that are associated with
3091+ a single partner and return a (account.id, partner.id) map
3092+ """
3093+ partner_ids = self.pool.get(
3094+ 'res.partner').search(cr, uid, [], context=context)
3095+ accounts_map = {}
3096+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
3097+ #
3098+ # Add the receivable account to the map
3099+ #
3100+ if accounts_map.get(partner.property_account_receivable.id, None) is None:
3101+ accounts_map[
3102+ partner.property_account_receivable.id] = partner.id
3103+ else:
3104+ # Two partners with the same receivable account: ignore
3105+ # this account!
3106+ accounts_map[partner.property_account_receivable.id] = 0
3107+ #
3108+ # Add the payable account to the map
3109+ #
3110+ if accounts_map.get(partner.property_account_payable.id, None) is None:
3111+ accounts_map[partner.property_account_payable.id] = partner.id
3112+ else:
3113+ # Two partners with the same receivable account: ignore
3114+ # this account!
3115+ accounts_map[partner.property_account_payable.id] = 0
3116+ return accounts_map
3117+
3118+ def action_skip_new(self, cr, uid, ids, context=None):
3119+ """
3120+ Action that just skips the to the ready state
3121+ """
3122+ return self._next_view(cr, uid, ids, 'view_set_partner_in_moves_ready_form', {'state': 'ready'}, context)
3123+
3124+ def action_find_moves_missing_partner(self, cr, uid, ids, context=None):
3125+ """
3126+ Action that searchs for account move lines of payable/receivable
3127+ accounts (of just one partner) that don't have the partner reference.
3128+ """
3129+ wiz = self.browse(cr, uid, ids[0], context)
3130+
3131+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
3132+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
3133+ if wiz.period_ids:
3134+ period_ids = [period.id for period in wiz.period_ids]
3135+ else:
3136+ period_ids = context and context.get('period_ids')
3137+
3138+ move_line_ids = []
3139+ accounts_map = self._get_accounts_map(cr, uid, context=context)
3140+
3141+ #
3142+ # Find the account move lines, of each of the accounts in the map
3143+ # that don't have a partner set.
3144+ #
3145+ query = """
3146+ SELECT id FROM account_move_line
3147+ WHERE account_id=%s
3148+ AND partner_id IS NULL
3149+ """
3150+ if period_ids:
3151+ query += """ AND period_id IN %s"""
3152+
3153+ for account_id in accounts_map.keys():
3154+ if accounts_map[account_id] != 0:
3155+ if period_ids:
3156+ cr.execute(query, (account_id, tuple(period_ids)))
3157+ else:
3158+ cr.execute(query, (account_id,))
3159+ new_move_line_ids = filter(
3160+ None, map(lambda x: x[0], cr.fetchall()))
3161+ if new_move_line_ids:
3162+ move_line_ids.extend(new_move_line_ids)
3163+
3164+ #
3165+ # Return the next view: Show 'ready' view
3166+ #
3167+ args = {
3168+ 'move_line_ids': move_line_ids,
3169+ 'state': 'ready',
3170+ }
3171+ return self._next_view(cr, uid, ids, 'view_set_partner_in_moves_ready_form', args, context)
3172+
3173+ def action_set_partner_in_moves(self, cr, uid, ids, context=None):
3174+ """
3175+ Action that sets for each partner payable/receivable account,
3176+ that is used only on one partner, the parner reference on its move
3177+ lines.
3178+ """
3179+ wiz = self.browse(cr, uid, ids[0], context)
3180+
3181+ # FIXME: The next block of code is a workaround to the lp:586252 bug of the 6.0 client.
3182+ # (https://bugs.launchpad.net/openobject-client/+bug/586252)
3183+ if wiz.move_line_ids:
3184+ move_line_ids = [line.id for line in wiz.move_line_ids]
3185+ else:
3186+ move_line_ids = context and context.get('move_line_ids')
3187+
3188+ accounts_map = self._get_accounts_map(cr, uid, context=context)
3189+
3190+ #
3191+ # Update the account move lines, of each of the accounts in the map
3192+ # that don't have a partner set, with the associated partner.
3193+ #
3194+ query = """
3195+ UPDATE account_move_line
3196+ SET partner_id=%s
3197+ WHERE id=%s
3198+ AND partner_id IS NULL
3199+ """
3200+ for line in self.pool.get('account.move.line').browse(cr, uid, move_line_ids, context=context):
3201+ if accounts_map[line.account_id.id] != 0:
3202+ cr.execute(query, (accounts_map[line.account_id.id], line.id))
3203+
3204+ #
3205+ # Return the next view: Show 'done' view
3206+ #
3207+ args = {
3208+ 'move_line_ids': move_line_ids,
3209+ 'state': 'done',
3210+ }
3211+ return self._next_view(cr, uid, ids, 'view_set_partner_in_moves_done_form', args, context)
3212+
3213+
3214+set_partner_in_moves()
3215
3216=== added file 'account_admin_tools/set_partner_in_moves.xml'
3217--- account_admin_tools/set_partner_in_moves.xml 1970-01-01 00:00:00 +0000
3218+++ account_admin_tools/set_partner_in_moves.xml 2012-12-07 10:00:37 +0000
3219@@ -0,0 +1,82 @@
3220+<?xml version="1.0" encoding="utf-8"?>
3221+<openerp>
3222+ <data>
3223+ <record id="view_set_partner_in_moves_form" model="ir.ui.view">
3224+ <field name="name">set_partner_in_moves.form</field>
3225+ <field name="model">account_admin_tools.set_partner_in_moves</field>
3226+ <field name="type">form</field>
3227+ <field name="arch" type="xml">
3228+ <form string="Set Partner Reference in Moves">
3229+ <label string="This wizard will set the partner reference in moves where the receivable/payable account (of a single partner) is used." colspan="4"/>
3230+ <label string="" colspan="4"/>
3231+ <group colspan="4">
3232+ <separator string="Find moves with missing partner reference" colspan="4"/>
3233+ <label string="You may now search for move lines with missing partner reference on the given periods, or you may skip this step and select the move lines by hand." colspan="4"/>
3234+ <label string="" colspan="4"/>
3235+ <newline/>
3236+ <field name="period_ids" colspan="4"/>
3237+ <label string="" colspan="4"/>
3238+ <newline/>
3239+ <button string="Search for moves" name="action_find_moves_missing_partner" type="object" icon="gtk-ok" colspan="4"/>
3240+ </group>
3241+ <label string="" colspan="4"/>
3242+ <group colspan="4">
3243+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
3244+ <button string="Skip" name="action_skip_new" type="object" icon="gtk-go-forward"/>
3245+ </group>
3246+ </form>
3247+ </field>
3248+ </record>
3249+
3250+ <record id="view_set_partner_in_moves_ready_form" model="ir.ui.view">
3251+ <field name="name">set_partner_in_moves.ready.form</field>
3252+ <field name="model">account_admin_tools.set_partner_in_moves</field>
3253+ <field name="type">form</field>
3254+ <field name="arch" type="xml">
3255+ <form string="Set Partner Reference in Moves">
3256+ <label string="The partner reference will be set, for the selected account move lines, to the one associated with the receivable/payable account." colspan="4"/>
3257+ <label string="" colspan="4"/>
3258+ <separator string="Account move lines to update" colspan="4"/>
3259+ <field name="move_line_ids" colspan="4" nolabel="1" domain="[('partner_id','=',False), ('account_id.type', 'in', ['receivable', 'payable'])]"/>
3260+ <label string="" colspan="4"/>
3261+ <group colspan="4">
3262+ <button string="Cancel" special="cancel" icon="gtk-cancel"/>
3263+ <button string="Set partner in moves" name="action_set_partner_in_moves" type="object" icon="gtk-apply"/>
3264+ </group>
3265+ </form>
3266+ </field>
3267+ </record>
3268+
3269+ <record id="view_set_partner_in_moves_done_form" model="ir.ui.view">
3270+ <field name="name">set_partner_in_moves.done.form</field>
3271+ <field name="model">account_admin_tools.set_partner_in_moves</field>
3272+ <field name="type">form</field>
3273+ <field name="arch" type="xml">
3274+ <form string="Set Partner Reference in Moves">
3275+ <label string="The partner references have been succesfully set on the account moves!" colspan="4"/>
3276+ <label string="" colspan="4"/>
3277+ <separator string="Updated account move lines" colspan="4"/>
3278+ <field name="move_line_ids" colspan="4" nolabel="1" domain="[('partner_id','=',False), ('account_id.type', 'in', ['receivable', 'payable'])]" readonly="1"/>
3279+ <label string="" colspan="4"/>
3280+ <group colspan="4">
3281+ <button string="Done" special="cancel" icon="gtk-ok"/>
3282+ </group>
3283+ </form>
3284+ </field>
3285+ </record>
3286+
3287+ <record id="action_set_partner_in_moves" model="ir.actions.act_window">
3288+ <field name="name">Set Partner Reference in Moves</field>
3289+ <field name="res_model">account_admin_tools.set_partner_in_moves</field>
3290+ <field name="view_type">form</field>
3291+ <field name="view_mode">form</field>
3292+ <field name="view_id" ref="view_set_partner_in_moves_form"/>
3293+ <field name="target">new</field>
3294+ </record>
3295+ <menuitem id="menu_action_set_partner_in_moves"
3296+ parent="menu_action_account_admin_tools_repair"
3297+ action="action_set_partner_in_moves"
3298+ sequence="120"/>
3299+
3300+ </data>
3301+</openerp>
3302
3303=== added directory 'account_chart_update'
3304=== added file 'account_chart_update/__init__.py'
3305--- account_chart_update/__init__.py 1970-01-01 00:00:00 +0000
3306+++ account_chart_update/__init__.py 2012-12-07 10:00:37 +0000
3307@@ -0,0 +1,28 @@
3308+# -*- coding: utf-8 -*-
3309+##############################################################################
3310+#
3311+# Copyright (c) 2010 Zikzakmedia S.L. (http://www.zikzakmedia.com)
3312+# Copyright (c) 2010 Pexego Sistemas Informáticos S.L. (http://www.pexego.es)
3313+# @authors: Jordi Esteve (Zikzakmedia), Borja López Soilán (Pexego)
3314+#
3315+# This program is free software: you can redistribute it and/or modify
3316+# it under the terms of the GNU Affero General Public License as published
3317+# by the Free Software Foundation, either version 3 of the License, or
3318+# (at your option) any later version.
3319+#
3320+# This program is distributed in the hope that it will be useful,
3321+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3322+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3323+# GNU Affero General Public License for more details.
3324+#
3325+# You should have received a copy of the GNU Affero General Public License
3326+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3327+#
3328+##############################################################################
3329+"""
3330+Account Chart Update Wizard
3331+"""
3332+__authors__ = ["Jordi Esteve <jesteve@zikzakmedia.com>",
3333+ "Borja López Soilán <borjals@pexego.es>"]
3334+
3335+import account
3336
3337=== added file 'account_chart_update/__openerp__.py'
3338--- account_chart_update/__openerp__.py 1970-01-01 00:00:00 +0000
3339+++ account_chart_update/__openerp__.py 2012-12-07 10:00:37 +0000
3340@@ -0,0 +1,64 @@
3341+# -*- coding: utf-8 -*-
3342+##############################################################################
3343+#
3344+# Copyright (c) 2010 Zikzakmedia S.L. (http://www.zikzakmedia.com)
3345+# Copyright (c) 2010 Pexego Sistemas Informáticos S.L. (http://www.pexego.es)
3346+# @authors: Jordi Esteve (Zikzakmedia), Borja López Soilán (Pexego)
3347+#
3348+# This program is free software: you can redistribute it and/or modify
3349+# it under the terms of the GNU Affero General Public License as published
3350+# by the Free Software Foundation, either version 3 of the License, or
3351+# (at your option) any later version.
3352+#
3353+# This program is distributed in the hope that it will be useful,
3354+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3355+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3356+# GNU Affero General Public License for more details.
3357+#
3358+# You should have received a copy of the GNU Affero General Public License
3359+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3360+#
3361+##############################################################################
3362+{
3363+ "name": "Detect changes and update the Account Chart from a template",
3364+ "version": "6.1",
3365+ "author": "Zikzakmedia SL",
3366+ "website": "www.zikzakmedia.com",
3367+ "license": "GPL-3",
3368+ "depends": ["account"],
3369+ "category": "Generic Modules/Accounting",
3370+ "description": """
3371+Adds a wizard to update a company account chart from a chart template.
3372+
3373+This is a pretty useful tool to update OpenERP instalations after tax reforms
3374+on the oficial charts of accounts, or to apply fixes performed on the chart
3375+template.
3376+
3377+The wizard:
3378+
3379+- Allows the user to compare a chart and a template showing differences
3380+ on accounts, taxes, tax codes and fiscal positions.
3381+- It may create the new account, taxes, tax codes and fiscal positions detected
3382+ on the template.
3383+- It can also update (overwrite) the accounts, taxes, tax codes and fiscal
3384+ positions that got modified on the template.
3385+
3386+The wizard lets the user select what kind of objects must be checked/updated,
3387+and whether old records must be checked for changes and updated.
3388+It will display all the accounts to be created / updated with some information
3389+about the detected differences, and allow the user to exclude records
3390+individually.
3391+Any problem found while updating will be shown on the last step.
3392+
3393+Authors:
3394+ Jordi Esteve (Zikzakmedia) <jesteve@zikzakmedia.com>
3395+ Borja López Soilán (Pexego) <borjals@pexego.es>
3396+""",
3397+ "init_xml": [],
3398+ "demo_xml": [],
3399+ "update_xml": [
3400+ "account_view.xml",
3401+ ],
3402+ "active": False,
3403+ "installable": True
3404+}
3405
3406=== added file 'account_chart_update/account.py'
3407--- account_chart_update/account.py 1970-01-01 00:00:00 +0000
3408+++ account_chart_update/account.py 2012-12-07 10:00:37 +0000
3409@@ -0,0 +1,1329 @@
3410+# -*- coding: utf-8 -*-
3411+##############################################################################
3412+#
3413+# Copyright (c) 2010 Zikzakmedia S.L. (http://www.zikzakmedia.com)
3414+# Copyright (c) 2010 Pexego Sistemas Informáticos S.L. (http://www.pexego.es)
3415+# @authors: Jordi Esteve (Zikzakmedia), Borja López Soilán (Pexego)
3416+#
3417+# This program is free software: you can redistribute it and/or modify
3418+# it under the terms of the GNU Affero General Public License as published
3419+# by the Free Software Foundation, either version 3 of the License, or
3420+# (at your option) any later version.
3421+#
3422+# This program is distributed in the hope that it will be useful,
3423+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3424+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3425+# GNU Affero General Public License for more details.
3426+#
3427+# You should have received a copy of the GNU Affero General Public License
3428+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3429+#
3430+##############################################################################
3431+"""
3432+Account Chart Update Wizard
3433+"""
3434+__authors__ = ["Jordi Esteve <jesteve@zikzakmedia.com>",
3435+ "Borja López Soilán <borjals@pexego.es>"]
3436+
3437+from osv import fields, osv
3438+from tools.translate import _
3439+import logging
3440+
3441+
3442+class WizardLog:
3443+ """
3444+ Small helper class to store the messages and errors on the wizard.
3445+ """
3446+ def __init__(self):
3447+ self.messages = []
3448+ self.errors = []
3449+
3450+ def add(self, message, is_error=False):
3451+ """
3452+ Adds a message to the log.
3453+ """
3454+ logger = logging.getLogger("account_chart_update")
3455+ if is_error:
3456+ logger.warning(u"Log line: %s" % message)
3457+ self.errors.append(message)
3458+ else:
3459+ logger.debug(u"Log line: %s" % message)
3460+ self.messages.append(message)
3461+
3462+ def has_errors(self):
3463+ """
3464+ Returns whether errors where logged.
3465+ """
3466+ return self.errors
3467+
3468+ def __call__(self):
3469+ return "".join(self.messages)
3470+
3471+ def __str__(self):
3472+ return "".join(self.messages)
3473+
3474+ def get_errors_str(self):
3475+ return "".join(self.errors)
3476+
3477+
3478+class wizard_update_charts_accounts(osv.osv_memory):
3479+ """
3480+ Updates an existing account chart for a company.
3481+ Wizards ask for:
3482+ * a company
3483+ * an account chart template
3484+ * a number of digits for formatting code of non-view accounts
3485+ Then, the wizard:
3486+ * generates/updates all accounts from the template and assigns them to the right company
3487+ * generates/updates all taxes and tax codes, changing account assignations
3488+ * generates/updates all accounting properties and assigns them correctly
3489+ """
3490+ _name = 'wizard.update.charts.accounts'
3491+
3492+ def _get_lang_selection_options(self, cr, uid, context={}):
3493+ """
3494+ Gets the available languages for the selection.
3495+ """
3496+ obj = self.pool.get('res.lang')
3497+ ids = obj.search(cr, uid, [], context=context)
3498+ res = obj.read(cr, uid, ids, ['code', 'name'], context)
3499+ return [(r['code'], r['name']) for r in res] + [('', '')]
3500+
3501+ _columns = {
3502+ 'state': fields.selection([
3503+ ('init', 'Step 1'),
3504+ ('ready', 'Step 2'),
3505+ ('done', 'Wizard Complete')
3506+ ], 'Status', readonly=True),
3507+ 'company_id': fields.many2one('res.company', 'Company', required=True),
3508+ 'chart_template_id': fields.many2one('account.chart.template', 'Chart Template', required=True),
3509+ 'code_digits': fields.integer('# of Digits', required=True, help="No. of Digits to use for account code. Make sure it is the same number as existing accounts."),
3510+ 'lang': fields.selection(_get_lang_selection_options, 'Language', size=5, help="For records searched by name (taxes, tax codes, fiscal positions), the template name will be matched against the record name on this language."),
3511+ 'update_tax_code': fields.boolean('Update tax codes', help="Existing tax codes are updated. Tax codes are searched by name."),
3512+ 'update_tax': fields.boolean('Update taxes', help="Existing taxes are updated. Taxes are searched by name."),
3513+ 'update_account': fields.boolean('Update accounts', help="Existing accounts are updated. Accounts are searched by code."),
3514+ 'update_fiscal_position': fields.boolean('Update fiscal positions', help="Existing fiscal positions are updated. Fiscal positions are searched by name."),
3515+ 'update_children_accounts_parent': fields.boolean("Update children accounts parent",
3516+ help="Update the parent of accounts that seem (based on the code) to be children of the newly created ones. If you had an account 430 with a child 4300000, and a 4300 account is created, the 4300000 parent will be set to 4300."),
3517+ 'continue_on_errors': fields.boolean("Continue on errors", help="If set, the wizard will continue to the next step even if there are minor errors (for example the parent account of a new account couldn't be set)."),
3518+ 'tax_code_ids': fields.one2many('wizard.update.charts.accounts.tax.code', 'update_chart_wizard_id', 'Tax codes'),
3519+ 'tax_ids': fields.one2many('wizard.update.charts.accounts.tax', 'update_chart_wizard_id', 'Taxes'),
3520+ 'account_ids': fields.one2many('wizard.update.charts.accounts.account', 'update_chart_wizard_id', 'Accounts'),
3521+ 'fiscal_position_ids': fields.one2many('wizard.update.charts.accounts.fiscal.position', 'update_chart_wizard_id', 'Fiscal positions'),
3522+ 'new_tax_codes': fields.integer('New tax codes', readonly=True),
3523+ 'new_taxes': fields.integer('New taxes', readonly=True),
3524+ 'new_accounts': fields.integer('New accounts', readonly=True),
3525+ 'new_fps': fields.integer('New fiscal positions', readonly=True),
3526+ 'updated_tax_codes': fields.integer('Updated tax codes', readonly=True),
3527+ 'updated_taxes': fields.integer('Updated taxes', readonly=True),
3528+ 'updated_accounts': fields.integer('Updated accounts', readonly=True),
3529+ 'updated_fps': fields.integer('Updated fiscal positions', readonly=True),
3530+ 'log': fields.text('Messages and Errors', readonly=True)
3531+ }
3532+
3533+ def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
3534+ """
3535+ Redefine the search to search by company name.
3536+ """
3537+ if not name:
3538+ name = '%'
3539+ if not args:
3540+ args = []
3541+ if not context:
3542+ context = {}
3543+ args = args[:]
3544+ ids = []
3545+ ids = self.search(
3546+ cr, user, [('company_id', operator, name)] + args, limit=limit)
3547+ return self.name_get(cr, user, ids, context=context)
3548+
3549+ def name_get(self, cr, uid, ids, context=None):
3550+ """
3551+ Use the company name and template as name.
3552+ """
3553+ if context is None:
3554+ context = {}
3555+ if not len(ids):
3556+ return []
3557+ records = self.browse(cr, uid, ids, context)
3558+ res = []
3559+ for record in records:
3560+ res.append((record.id, record.company_id.name +
3561+ ' - ' + record.chart_template_id.name))
3562+ return res
3563+
3564+ def _get_chart(self, cr, uid, context=None):
3565+ """
3566+ Returns the default chart template.
3567+ """
3568+ if context is None:
3569+ context = {}
3570+ ids = self.pool.get(
3571+ 'account.chart.template').search(cr, uid, [], context=context)
3572+ if ids:
3573+ return ids[0]
3574+ return False
3575+
3576+ def _get_code_digits(self, cr, uid, context=None, company_id=None):
3577+ """
3578+ Returns the default code size for the accounts.
3579+
3580+ To figure out the number of digits of the accounts it look at the
3581+ code size of the default receivable account of the company
3582+ (or user's company if any company is given).
3583+ """
3584+ if context is None:
3585+ context = {}
3586+ property_obj = self.pool.get('ir.property')
3587+ account_obj = self.pool.get('account.account')
3588+ if not company_id:
3589+ user = self.pool.get('res.users').browse(cr, uid, uid, context)
3590+ company_id = user.company_id.id
3591+ property_ids = property_obj.search(cr, uid, [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id), ('res_id', '=', False), ('value_reference', '!=', False)])
3592+ if not property_ids:
3593+ # Try to get a generic (no-company) property
3594+ property_ids = property_obj.search(cr, uid, [('name', '=', 'property_account_receivable'), ('res_id', '=', False), ('value_reference', '!=', False)])
3595+ number_digits = 6
3596+ if property_ids:
3597+ prop = property_obj.browse(
3598+ cr, uid, property_ids[0], context=context)
3599+ account_id = prop.value_reference.id
3600+
3601+ if account_id:
3602+ code = account_obj.read(
3603+ cr, uid, account_id, ['code'], context)['code']
3604+ number_digits = len(code)
3605+ return number_digits
3606+
3607+ _defaults = {
3608+ 'state': lambda *a: 'init',
3609+ 'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, [uid], context)[0].company_id.id,
3610+ 'chart_template_id': _get_chart,
3611+ 'update_tax_code': lambda *a: True,
3612+ 'update_tax': lambda *a: True,
3613+ 'update_account': lambda *a: True,
3614+ 'update_fiscal_position': lambda *a: True,
3615+ 'update_children_accounts_parent': lambda *a: True,
3616+ 'continue_on_errors': lambda *a: False,
3617+ 'lang': lambda self, cr, uid, context: context and context.get('lang') or None,
3618+ }
3619+
3620+ def onchange_company_id(self, cr, uid, ids, company_id, context=None):
3621+ """
3622+ Update the code size when the company changes
3623+ """
3624+ res = {
3625+ 'value': {
3626+ 'code_digits': self._get_code_digits(cr, uid, context=context, company_id=company_id),
3627+ }
3628+ }
3629+ return res
3630+
3631+ def action_init(self, cr, uid, ids, context=None):
3632+ """
3633+ Initial action that sets the initial state.
3634+ """
3635+ if context is None:
3636+ context = {}
3637+ self.write(cr, uid, ids, {'state': 'init'}, context)
3638+ return True
3639+
3640+ ############################################################################
3641+ # Helper methods
3642+ ##########################################################################
3643+ def _map_tax_template(self, cr, uid, wizard, tax_template_mapping, tax_template, context=None):
3644+ """
3645+ Adds a tax template -> tax id to the mapping.
3646+ """
3647+ if tax_template and not tax_template_mapping.get(tax_template.id):
3648+ tax_facade = self.pool.get('account.tax')
3649+ tax_ids = tax_facade.search(cr, uid, [
3650+ ('name', '=', tax_template.name),
3651+ ('company_id', '=', wizard.company_id.id)
3652+ ], context=context)
3653+ if tax_ids:
3654+ tax_template_mapping[tax_template.id] = tax_ids[0]
3655+
3656+ def _map_tax_code_template(self, cr, uid, wizard, tax_code_template_mapping, tax_code_template, context=None):
3657+ """
3658+ Adds a tax code template -> tax code id to the mapping.
3659+ """
3660+ if tax_code_template and not tax_code_template_mapping.get(tax_code_template.id):
3661+ tax_code_facade = self.pool.get('account.tax.code')
3662+ root_tax_code_id = wizard.chart_template_id.tax_code_root_id.id
3663+ tax_code_name = (tax_code_template.id == root_tax_code_id) and wizard.company_id.name or tax_code_template.name
3664+ tax_code_ids = tax_code_facade.search(cr, uid, [
3665+ ('name', '=', tax_code_name),
3666+ ('company_id', '=', wizard.company_id.id)
3667+ ])
3668+ if tax_code_ids:
3669+ tax_code_template_mapping[
3670+ tax_code_template.id] = tax_code_ids[0]
3671+
3672+ def _map_account_template(self, cr, uid, wizard, account_template_mapping, account_template, context=None):
3673+ """
3674+ Adds an account template -> account id to the mapping
3675+ """
3676+ if account_template and not account_template_mapping.get(account_template.id):
3677+ account_facade = self.pool.get('account.account')
3678+ code = account_template.code or ''
3679+ if account_template.type != 'view':
3680+ if len(code) > 0 and len(code) <= wizard.code_digits:
3681+ code = '%s%s' % (
3682+ code, '0' * (wizard.code_digits - len(code)))
3683+ account_ids = account_facade.search(cr, uid, [
3684+ ('code', '=', code),
3685+ ('company_id', '=', wizard.company_id.id)
3686+ ], context=context)
3687+ if account_ids:
3688+ account_template_mapping[account_template.id] = account_ids[0]
3689+
3690+ def _map_fp_template(self, cr, uid, wizard, fp_template_mapping, fp_template, context=None):
3691+ """
3692+ Adds a fiscal position template -> fiscal position id to the mapping.
3693+ """
3694+ if fp_template and not fp_template_mapping.get(fp_template.id):
3695+ fp_facade = self.pool.get('account.fiscal.position')
3696+ fp_ids = fp_facade.search(cr, uid, [
3697+ ('name', '=', fp_template.name),
3698+ ('company_id', '=', wizard.company_id.id)
3699+ ], context=context)
3700+ if fp_ids:
3701+ fp_template_mapping[fp_template.id] = fp_ids[0]
3702+
3703+ ############################################################################
3704+ # Find records methods
3705+ ##########################################################################
3706+ def _find_tax_codes(self, cr, uid, wizard, context=None):
3707+ """
3708+ Search for, and load, tax code templates to create/update.
3709+ """
3710+ new_tax_codes = 0
3711+ updated_tax_codes = 0
3712+ tax_code_template_mapping = {}
3713+
3714+ tax_code_templ_facade = self.pool.get('account.tax.code.template')
3715+ tax_code_facade = self.pool.get('account.tax.code')
3716+ wiz_tax_code_facade = self.pool.get(
3717+ 'wizard.update.charts.accounts.tax.code')
3718+
3719+ # Remove previous tax codes
3720+ wiz_tax_code_facade.unlink(
3721+ cr, uid, wiz_tax_code_facade.search(cr, uid, []))
3722+
3723+ #
3724+ # Search for new / updated tax codes
3725+ #
3726+ root_tax_code_id = wizard.chart_template_id.tax_code_root_id.id
3727+ children_tax_code_template = tax_code_templ_facade.search(cr, uid, [(
3728+ 'parent_id', 'child_of', [root_tax_code_id])], order='id')
3729+ for tax_code_template in tax_code_templ_facade.browse(cr, uid, children_tax_code_template):
3730+ # Ensure the tax code template is on the map (search for the mapped
3731+ # tax code id).
3732+ self._map_tax_code_template(cr, uid, wizard, tax_code_template_mapping, tax_code_template, context)
3733+
3734+ tax_code_id = tax_code_template_mapping.get(tax_code_template.id)
3735+ if not tax_code_id:
3736+ new_tax_codes += 1
3737+ wiz_tax_code_facade.create(cr, uid, {
3738+ 'tax_code_id': tax_code_template.id,
3739+ 'update_chart_wizard_id': wizard.id,
3740+ 'type': 'new',
3741+ }, context)
3742+ elif wizard.update_tax_code:
3743+ #
3744+ # Check the tax code for changes.
3745+ #
3746+ modified = False
3747+ notes = ""
3748+ tax_code = tax_code_facade.browse(
3749+ cr, uid, tax_code_id, context=context)
3750+
3751+ if tax_code.code != tax_code_template.code:
3752+ notes += _("The code field is different.\n")
3753+ modified = True
3754+ if tax_code.info != tax_code_template.info:
3755+ notes += _("The info field is different.\n")
3756+ modified = True
3757+ if tax_code.sign != tax_code_template.sign:
3758+ notes += _("The sign field is different.\n")
3759+ modified = True
3760+
3761+ # TODO: We could check other account fields for changes...
3762+
3763+ if modified:
3764+ #
3765+ # Tax code to update.
3766+ #
3767+ updated_tax_codes += 1
3768+ wiz_tax_code_facade.create(cr, uid, {
3769+ 'tax_code_id': tax_code_template.id,
3770+ 'update_chart_wizard_id': wizard.id,
3771+ 'type': 'updated',
3772+ 'update_tax_code_id': tax_code_id,
3773+ 'notes': notes,
3774+ }, context)
3775+
3776+ return {'new': new_tax_codes, 'updated': updated_tax_codes, 'mapping': tax_code_template_mapping}
3777+
3778+ def _find_taxes(self, cr, uid, wizard, context=None):
3779+ """
3780+ Search for, and load, tax templates to create/update.
3781+ """
3782+ new_taxes = 0
3783+ updated_taxes = 0
3784+ tax_template_mapping = {}
3785+
3786+ tax_facade = self.pool.get('account.tax')
3787+ wiz_tax_facade = self.pool.get('wizard.update.charts.accounts.tax')
3788+
3789+ delay_wiz_tax = []
3790+ # Remove previous taxes
3791+ wiz_tax_facade.unlink(cr, uid, wiz_tax_facade.search(cr, uid, []))
3792+
3793+ #
3794+ # Search for new / updated taxes
3795+ #
3796+ for tax_template in wizard.chart_template_id.tax_template_ids:
3797+ # Ensure the tax template is on the map (search for the mapped tax
3798+ # id).
3799+ self._map_tax_template(
3800+ cr, uid, wizard, tax_template_mapping, tax_template, context)
3801+
3802+ tax_id = tax_template_mapping.get(tax_template.id)
3803+ if not tax_id:
3804+ new_taxes += 1
3805+ vals_wiz = {
3806+ 'tax_id': tax_template.id,
3807+ 'update_chart_wizard_id': wizard.id,
3808+ 'type': 'new',
3809+ }
3810+ if not tax_template.parent_id:
3811+ wiz_tax_facade.create(cr, uid, vals_wiz, context)
3812+ else:
3813+ delay_wiz_tax.append(vals_wiz)
3814+ elif wizard.update_tax:
3815+ #
3816+ # Check the tax for changes.
3817+ #
3818+ modified = False
3819+ notes = ""
3820+ tax = tax_facade.browse(cr, uid, tax_id, context=context)
3821+
3822+ if tax.sequence != tax_template.sequence:
3823+ notes += _("The sequence field is different.\n")
3824+ modified = True
3825+ if tax.amount != tax_template.amount:
3826+ notes += _("The amount field is different.\n")
3827+ modified = True
3828+ if tax.type != tax_template.type:
3829+ notes += _("The type field is different.\n")
3830+ modified = True
3831+ if tax.applicable_type != tax_template.applicable_type:
3832+ notes += _("The applicable type field is different.\n")
3833+ modified = True
3834+ if tax.domain != tax_template.domain:
3835+ notes += _("The domain field is different.\n")
3836+ modified = True
3837+ if tax.child_depend != tax_template.child_depend:
3838+ notes += _("The child depend field is different.\n")
3839+ modified = True
3840+ if tax.python_compute != tax_template.python_compute:
3841+ notes += _("The python compute field is different.\n")
3842+ modified = True
3843+ # if tax.tax_group != tax_template.tax_group:
3844+ # notes += _("The tax group field is different.\n")
3845+ # modified = True
3846+ if tax.base_sign != tax_template.base_sign:
3847+ notes += _("The base sign field is different.\n")
3848+ modified = True
3849+ if tax.tax_sign != tax_template.tax_sign:
3850+ notes += _("The tax sign field is different.\n")
3851+ modified = True
3852+ if tax.include_base_amount != tax_template.include_base_amount:
3853+ notes += _("The include base amount field is different.\n")
3854+ modified = True
3855+ if tax.type_tax_use != tax_template.type_tax_use:
3856+ notes += _("The type tax use field is different.\n")
3857+ modified = True
3858+ # TODO: We could check other tax fields for changes...
3859+
3860+ if modified:
3861+ #
3862+ # Tax code to update.
3863+ #
3864+ updated_taxes += 1
3865+ wiz_tax_facade.create(cr, uid, {
3866+ 'tax_id': tax_template.id,
3867+ 'update_chart_wizard_id': wizard.id,
3868+ 'type': 'updated',
3869+ 'update_tax_id': tax_id,
3870+ 'notes': notes,
3871+ }, context)
3872+
3873+ for delay_vals_wiz in delay_wiz_tax:
3874+ wiz_tax_facade.create(cr, uid, delay_vals_wiz, context)
3875+
3876+ return {'new': new_taxes, 'updated': updated_taxes, 'mapping': tax_template_mapping}
3877+
3878+ def _find_accounts(self, cr, uid, wizard, context=None):
3879+ """
3880+ Search for, and load, account templates to create/update.
3881+ """
3882+ new_accounts = 0
3883+ updated_accounts = 0
3884+ account_template_mapping = {}
3885+
3886+ account_facade = self.pool.get('account.account')
3887+ account_template_facade = self.pool.get('account.account.template')
3888+ wiz_account_facade = self.pool.get(
3889+ 'wizard.update.charts.accounts.account')
3890+
3891+ # Remove previous accounts
3892+ wiz_account_facade.unlink(
3893+ cr, uid, wiz_account_facade.search(cr, uid, []))
3894+
3895+ #
3896+ # Search for new / updated accounts
3897+ #
3898+ root_account_id = wizard.chart_template_id.account_root_id.id
3899+ children_acc_template = account_template_facade.search(cr, uid, [(
3900+ 'parent_id', 'child_of', [root_account_id])], context=context)
3901+ children_acc_template.sort()
3902+ for account_template in account_template_facade.browse(cr, uid, children_acc_template, context=context):
3903+ # Ensure the account template is on the map (search for the mapped
3904+ # account id).
3905+ self._map_account_template(cr, uid, wizard, account_template_mapping, account_template, context)
3906+
3907+ account_id = account_template_mapping.get(account_template.id)
3908+ if not account_id:
3909+ new_accounts += 1
3910+ wiz_account_facade.create(cr, uid, {
3911+ 'account_id': account_template.id,
3912+ 'update_chart_wizard_id': wizard.id,
3913+ 'type': 'new',
3914+ }, context)
3915+ elif wizard.update_account:
3916+ #
3917+ # Check the account for changes.
3918+ #
3919+ modified = False
3920+ notes = ""
3921+ account = account_facade.browse(
3922+ cr, uid, account_id, context=context)
3923+
3924+ if account.name != account_template.name and account.name != wizard.company_id.name:
3925+ notes += _("The name is different.\n")
3926+ modified = True
3927+ if account.type != account_template.type:
3928+ notes += _("The type is different.\n")
3929+ modified = True
3930+ if account.user_type != account_template.user_type:
3931+ notes += _("The user type is different.\n")
3932+ modified = True
3933+ if account.reconcile != account_template.reconcile:
3934+ notes += _("The reconcile is different.\n")
3935+ modified = True
3936+
3937+ # TODO: We could check other account fields for changes...
3938+
3939+ if modified:
3940+ #
3941+ # Account to update.
3942+ #
3943+ updated_accounts += 1
3944+ wiz_account_facade.create(cr, uid, {
3945+ 'account_id': account_template.id,
3946+ 'update_chart_wizard_id': wizard.id,
3947+ 'type': 'updated',
3948+ 'update_account_id': account_id,
3949+ 'notes': notes,
3950+ }, context)
3951+
3952+ return {'new': new_accounts, 'updated': updated_accounts, 'mapping': account_template_mapping}
3953+
3954+ def _find_fiscal_positions(self, cr, uid, wizard, context=None):
3955+ """
3956+ Search for, and load, fiscal position templates to create/update.
3957+ """
3958+ new_fps = 0
3959+ updated_fps = 0
3960+ fp_template_mapping = {}
3961+
3962+ fp_template_facade = self.pool.get('account.fiscal.position.template')
3963+ fp_facade = self.pool.get('account.fiscal.position')
3964+ wiz_fp_facade = self.pool.get(
3965+ 'wizard.update.charts.accounts.fiscal.position')
3966+
3967+ # Remove previous fiscal positions
3968+ wiz_fp_facade.unlink(cr, uid, wiz_fp_facade.search(cr, uid, []))
3969+
3970+ #
3971+ # Search for new / updated fiscal positions
3972+ #
3973+ fp_template_ids = fp_template_facade.search(cr, uid, [('chart_template_id', '=', wizard.chart_template_id.id)], context=context)
3974+ for fp_template in fp_template_facade.browse(cr, uid, fp_template_ids, context=context):
3975+ # Ensure the fiscal position template is on the map (search for the
3976+ # mapped fiscal position id).
3977+ self._map_fp_template(
3978+ cr, uid, wizard, fp_template_mapping, fp_template, context)
3979+
3980+ fp_id = fp_template_mapping.get(fp_template.id)
3981+ if not fp_id:
3982+ #
3983+ # New fiscal position template.
3984+ #
3985+ new_fps += 1
3986+ wiz_fp_facade.create(cr, uid, {
3987+ 'fiscal_position_id': fp_template.id,
3988+ 'update_chart_wizard_id': wizard.id,
3989+ 'type': 'new',
3990+ }, context)
3991+ elif wizard.update_fiscal_position:
3992+ #
3993+ # Check the fiscal position for changes.
3994+ #
3995+ modified = False
3996+ notes = ""
3997+ fp = fp_facade.browse(cr, uid, fp_id, context=context)
3998+
3999+ #
4000+ # Check fiscal position taxes for changes.
4001+ #
4002+ if fp_template.tax_ids and fp.tax_ids:
4003+ for fp_tax_template in fp_template.tax_ids:
4004+ found = False
4005+ for fp_tax in fp.tax_ids:
4006+ if fp_tax.tax_src_id.name == fp_tax_template.tax_src_id.name:
4007+ if fp_tax_template.tax_dest_id and fp_tax.tax_dest_id:
4008+ if fp_tax.tax_dest_id.name == fp_tax_template.tax_dest_id.name:
4009+ found = True
4010+ break
4011+ elif not fp_tax_template.tax_dest_id and not fp_tax.tax_dest_id:
4012+ found = True
4013+ break
4014+ if not found:
4015+ if fp_tax_template.tax_dest_id:
4016+ notes += _("Tax mapping not found on the fiscal position instance: %s -> %s.\n") % (fp_tax_template.tax_src_id.name, fp_tax_template.tax_dest_id.name)
4017+ else:
4018+ notes += _("Tax mapping not found on the fiscal position instance: %s -> None.\n") % fp_tax_template.tax_src_id.name
4019+ modified = True
4020+ elif fp_template.tax_ids and not fp.tax_ids:
4021+ notes += _("The template has taxes the fiscal position instance does not.\n")
4022+ modified = True
4023+
4024+ #
4025+ # Check fiscal position accounts for changes.
4026+ #
4027+ if fp_template.account_ids and fp.account_ids:
4028+ for fp_account_template in fp_template.account_ids:
4029+ found = False
4030+ for fp_account in fp.account_ids:
4031+ if fp_account.account_src_id.name == fp_account_template.account_src_id.name:
4032+ if fp_account.account_dest_id.name == fp_account_template.account_dest_id.name:
4033+ found = True
4034+ break
4035+ if not found:
4036+ notes += _("Account mapping not found on the fiscal position instance: %s -> %s.\n") % (fp_account_template.account_src_id.name, fp_account_template.account_dest_id.name)
4037+ modified = True
4038+ elif fp_template.account_ids and not fp.account_ids:
4039+ notes += _("The template has accounts the fiscal position instance does not.\n")
4040+ modified = True
4041+
4042+ if modified:
4043+ #
4044+ # Fiscal position template to update.
4045+ #
4046+ updated_fps += 1
4047+ wiz_fp_facade.create(cr, uid, {
4048+ 'fiscal_position_id': fp_template.id,
4049+ 'update_chart_wizard_id': wizard.id,
4050+ 'type': 'updated',
4051+ 'update_fiscal_position_id': fp_id,
4052+ 'notes': notes,
4053+ }, context)
4054+
4055+ return {'new': new_fps, 'updated': updated_fps, 'mapping': fp_template_mapping}
4056+
4057+ def action_find_records(self, cr, uid, ids, context=None):
4058+ """
4059+ Searchs for records to update/create and shows them
4060+ """
4061+ if context is None:
4062+ context = {}
4063+ wizard = self.browse(cr, uid, ids[0], context=context)
4064+
4065+ if wizard.lang:
4066+ context['lang'] = wizard.lang
4067+ elif context.get('lang'):
4068+ del context['lang']
4069+
4070+ #
4071+ # Search for, and load, the records to create/update.
4072+ #
4073+ tax_codes_res = self._find_tax_codes(cr, uid, wizard, context=context)
4074+ taxes_res = self._find_taxes(cr, uid, wizard, context=context)
4075+ accounts_res = self._find_accounts(cr, uid, wizard, context=context)
4076+ fps_res = self._find_fiscal_positions(cr, uid, wizard, context=context)
4077+
4078+ #
4079+ # Write the results, and go to the next step.
4080+ #
4081+ self.write(cr, uid, [wizard.id], {
4082+ 'state': 'ready',
4083+ 'new_tax_codes': tax_codes_res.get('new', 0),
4084+ 'new_taxes': taxes_res.get('new', 0),
4085+ 'new_accounts': accounts_res.get('new', 0),
4086+ 'new_fps': fps_res.get('new', 0),
4087+ 'updated_tax_codes': tax_codes_res.get('updated', 0),
4088+ 'updated_taxes': taxes_res.get('updated', 0),
4089+ 'updated_accounts': accounts_res.get('updated', 0),
4090+ 'updated_fps': fps_res.get('updated', 0),
4091+ }, context)
4092+
4093+ return True
4094+
4095+ ############################################################################
4096+ # Update records methods
4097+ ##########################################################################
4098+ def _update_tax_codes(self, cr, uid, wizard, log, context=None):
4099+ """
4100+ Search for, and load, tax code templates to create/update.
4101+ """
4102+ tax_code_facade = self.pool.get('account.tax.code')
4103+
4104+ root_tax_code_id = wizard.chart_template_id.tax_code_root_id.id
4105+
4106+ new_tax_codes = 0
4107+ updated_tax_codes = 0
4108+ tax_code_template_mapping = {}
4109+
4110+ for wiz_tax_code in wizard.tax_code_ids:
4111+ tax_code_template = wiz_tax_code.tax_code_id
4112+ tax_code_name = (root_tax_code_id == tax_code_template.id) and wizard.company_id.name or tax_code_template.name
4113+
4114+ # Ensure the parent tax code template is on the map.
4115+ self._map_tax_code_template(cr, uid, wizard, tax_code_template_mapping, tax_code_template.parent_id, context)
4116+
4117+ #
4118+ # Values
4119+ #
4120+ vals = {
4121+ 'name': tax_code_name,
4122+ 'code': tax_code_template.code,
4123+ 'info': tax_code_template.info,
4124+ 'parent_id': tax_code_template.parent_id and tax_code_template_mapping.get(tax_code_template.parent_id.id),
4125+ 'company_id': wizard.company_id.id,
4126+ 'sign': tax_code_template.sign,
4127+ }
4128+
4129+ tax_code_id = None
4130+ modified = False
4131+
4132+ if wiz_tax_code.type == 'new':
4133+ #
4134+ # Create the tax code
4135+ #
4136+ tax_code_id = tax_code_facade.create(cr, uid, vals)
4137+ log.add(_("Created tax code %s.\n") % tax_code_name)
4138+ new_tax_codes += 1
4139+ modified = True
4140+ elif wizard.update_tax_code and wiz_tax_code.update_tax_code_id:
4141+ #
4142+ # Update the tax code
4143+ #
4144+ tax_code_id = wiz_tax_code.update_tax_code_id.id
4145+ tax_code_facade.write(cr, uid, [tax_code_id], vals)
4146+ log.add(_("Updated tax code %s.\n") % tax_code_name)
4147+ updated_tax_codes += 1
4148+ modified = True
4149+ else:
4150+ tax_code_id = wiz_tax_code.update_tax_code_id and wiz_tax_code.update_tax_code_id.id
4151+ modified = False
4152+
4153+ # Store the tax codes on the map
4154+ tax_code_template_mapping[tax_code_template.id] = tax_code_id
4155+
4156+ if modified:
4157+ #
4158+ # Detect errors
4159+ #
4160+ if tax_code_template.parent_id and not tax_code_template_mapping.get(tax_code_template.parent_id.id):
4161+ log.add(_("Tax code %s: The parent tax code %s can not be set.\n") % (tax_code_name, tax_code_template.parent_id.name), True)
4162+
4163+ return {
4164+ 'new': new_tax_codes,
4165+ 'updated': updated_tax_codes,
4166+ 'mapping': tax_code_template_mapping
4167+ }
4168+
4169+ def _update_taxes(self, cr, uid, wizard, log, tax_code_template_mapping, context=None):
4170+ """
4171+ Search for, and load, tax templates to create/update.
4172+ """
4173+ tax_facade = self.pool.get('account.tax')
4174+
4175+ new_taxes = 0
4176+ updated_taxes = 0
4177+ tax_template_mapping = {}
4178+ taxes_pending_for_accounts = {}
4179+
4180+ for wiz_tax in wizard.tax_ids:
4181+ tax_template = wiz_tax.tax_id
4182+
4183+ # Ensure the parent tax template is on the map.
4184+ self._map_tax_template(cr, uid, wizard, tax_template_mapping,
4185+ tax_template.parent_id, context)
4186+
4187+ #
4188+ # Ensure the referenced tax codes are on the map.
4189+ #
4190+ tax_code_templates_to_find = [
4191+ tax_template.base_code_id,
4192+ tax_template.tax_code_id,
4193+ tax_template.ref_base_code_id,
4194+ tax_template.ref_tax_code_id
4195+ ]
4196+ for tax_code_template in [tmpl for tmpl in tax_code_templates_to_find if tmpl]:
4197+ self._map_tax_code_template(cr, uid, wizard, tax_code_template_mapping, tax_code_template)
4198+
4199+ #
4200+ # Values
4201+ #
4202+ vals_tax = {
4203+ 'name': tax_template.name,
4204+ 'sequence': tax_template.sequence,
4205+ 'amount': tax_template.amount,
4206+ 'type': tax_template.type,
4207+ 'applicable_type': tax_template.applicable_type,
4208+ 'domain': tax_template.domain,
4209+ 'parent_id': tax_template.parent_id and tax_template_mapping.get(tax_template.parent_id.id),
4210+ 'child_depend': tax_template.child_depend,
4211+ 'python_compute': tax_template.python_compute,
4212+ 'python_compute_inv': tax_template.python_compute_inv,
4213+ 'python_applicable': tax_template.python_applicable,
4214+ #'tax_group': tax_template.tax_group,
4215+ 'base_code_id': tax_template.base_code_id and tax_code_template_mapping.get(tax_template.base_code_id.id),
4216+ 'tax_code_id': tax_template.tax_code_id and tax_code_template_mapping.get(tax_template.tax_code_id.id),
4217+ 'base_sign': tax_template.base_sign,
4218+ 'tax_sign': tax_template.tax_sign,
4219+ 'ref_base_code_id': tax_template.ref_base_code_id and tax_code_template_mapping.get(tax_template.ref_base_code_id.id),
4220+ 'ref_tax_code_id': tax_template.ref_tax_code_id and tax_code_template_mapping.get(tax_template.ref_tax_code_id.id),
4221+ 'ref_base_sign': tax_template.ref_base_sign,
4222+ 'ref_tax_sign': tax_template.ref_tax_sign,
4223+ 'include_base_amount': tax_template.include_base_amount,
4224+ 'description': tax_template.description,
4225+ 'company_id': wizard.company_id.id,
4226+ 'type_tax_use': tax_template.type_tax_use
4227+ }
4228+
4229+ tax_id = None
4230+ modified = False
4231+
4232+ if wiz_tax.type == 'new':
4233+ #
4234+ # Create a new tax.
4235+ #
4236+ tax_id = tax_facade.create(cr, uid, vals_tax)
4237+ log.add(_("Created tax %s.\n") % tax_template.name)
4238+ new_taxes += 1
4239+ modified = True
4240+ elif wizard.update_tax and wiz_tax.update_tax_id:
4241+ #
4242+ # Update a tax.
4243+ #
4244+ tax_id = wiz_tax.update_tax_id.id
4245+ tax_facade.write(cr, uid, [tax_id], vals_tax)
4246+ log.add(_("Updated tax %s.\n") % tax_template.name)
4247+ updated_taxes += 1
4248+ modified = True
4249+ else:
4250+ tax_id = wiz_tax.update_tax_id and wiz_tax.update_tax_id.id
4251+
4252+ # Update the tax template map
4253+ tax_template_mapping[tax_template.id] = tax_id
4254+
4255+ if modified:
4256+ #
4257+ # Add to the dict of taxes waiting for accounts.
4258+ #
4259+ taxes_pending_for_accounts[tax_id] = {
4260+ 'account_collected_id': tax_template.account_collected_id and tax_template.account_collected_id.id or False,
4261+ 'account_paid_id': tax_template.account_paid_id and tax_template.account_paid_id.id or False,
4262+ }
4263+
4264+ #
4265+ # Detect errors
4266+ #
4267+ if tax_template.parent_id and not tax_template_mapping.get(tax_template.parent_id.id):
4268+ log.add(_("Tax %s: The parent tax %s can not be set.\n") % (tax_template.name, tax_template.parent_id.name), True)
4269+ if tax_template.base_code_id and not tax_code_template_mapping.get(tax_template.base_code_id.id):
4270+ log.add(_("Tax %s: The tax code for the base %s can not be set.\n") % (tax_template.name, tax_template.base_code_id.name), True)
4271+ if tax_template.tax_code_id and not tax_code_template_mapping.get(tax_template.tax_code_id.id):
4272+ log.add(_("Tax %s: The tax code for the tax %s can not be set.\n") % (tax_template.name, tax_template.tax_code_id.name), True)
4273+ if tax_template.ref_base_code_id and not tax_code_template_mapping.get(tax_template.ref_base_code_id.id):
4274+ log.add(_("Tax %s: The tax code for the base refund %s can not be set.\n") % (tax_template.name, tax_template.ref_base_code_id.name), True)
4275+ if tax_template.ref_tax_code_id and not tax_code_template_mapping.get(tax_template.ref_tax_code_id.id):
4276+ log.add(_("Tax %s: The tax code for the tax refund %s can not be set.\n") % (tax_template.name, tax_template.ref_tax_code_id.name), True)
4277+
4278+ return {
4279+ 'new': new_taxes,
4280+ 'updated': updated_taxes,
4281+ 'mapping': tax_template_mapping,
4282+ 'pending': taxes_pending_for_accounts
4283+ }
4284+
4285+ def _update_children_accounts_parent(self, cr, uid, wizard, log, parent_account_id, context=None):
4286+ """
4287+ Updates the parent_id of accounts that seem to be children of the
4288+ given account (accounts that start with the same code and are brothers
4289+ of the first account).
4290+ """
4291+ account_facade = self.pool.get('account.account')
4292+ parent_account = account_facade.browse(
4293+ cr, uid, parent_account_id, context=context)
4294+
4295+ if not parent_account.parent_id or not parent_account.code:
4296+ return False
4297+
4298+ children_ids = account_facade.search(cr, uid, [
4299+ ('company_id', '=',
4300+ parent_account.company_id and parent_account.company_id.id),
4301+ ('parent_id', '=', parent_account.parent_id.id),
4302+ ('code', '=like', "%s%%" % parent_account.code),
4303+ ('id', '!=', parent_account.id),
4304+ ], context=context)
4305+
4306+ if children_ids:
4307+ try:
4308+ account_facade.write(cr, uid, children_ids, {'parent_id':
4309+ parent_account.id}, context=context)
4310+ except osv.except_osv, ex:
4311+ log.add(_("Exception setting the parent of account %s children: %s - %s.\n") % (parent_account.code, ex.name, ex.value), True)
4312+
4313+ return True
4314+
4315+ def _update_accounts(self, cr, uid, wizard, log, tax_template_mapping, context=None):
4316+ """
4317+ Search for, and load, account templates to create/update.
4318+ """
4319+ account_facade = self.pool.get('account.account')
4320+
4321+ root_account_id = wizard.chart_template_id.account_root_id.id
4322+
4323+ # Disable the parent_store computing on account_account during the batch
4324+ # processing, we will force _parent_store_compute afterwards.
4325+ self.pool._init = True
4326+
4327+ new_accounts = 0
4328+ updated_accounts = 0
4329+ account_template_mapping = {}
4330+
4331+ for wiz_account in wizard.account_ids:
4332+ account_template = wiz_account.account_id
4333+
4334+ # Ensure the parent account template is on the map.
4335+ self._map_account_template(cr, uid, wizard, account_template_mapping, account_template.parent_id, context)
4336+
4337+ #
4338+ # Ensure the related tax templates are on the map.
4339+ #
4340+ for tax_template in account_template.tax_ids:
4341+ self._map_tax_template(cr, uid, wizard, tax_template_mapping,
4342+ tax_template, context)
4343+
4344+ # Get the tax ids
4345+ tax_ids = [tax_template_mapping[tax_template.id] for tax_template in account_template.tax_ids if tax_template_mapping[tax_template.id]]
4346+
4347+ #
4348+ # Calculate the account code (we need to add zeros to non-view
4349+ # account codes)
4350+ #
4351+ code = account_template.code or ''
4352+ if account_template.type != 'view':
4353+ if len(code) > 0 and len(code) <= wizard.code_digits:
4354+ code = '%s%s' % (
4355+ code, '0' * (wizard.code_digits - len(code)))
4356+
4357+ #
4358+ # Values
4359+ #
4360+ vals = {
4361+ 'name': (root_account_id == account_template.id) and wizard.company_id.name or account_template.name,
4362+ #'sign': account_template.sign,
4363+ 'currency_id': account_template.currency_id and account_template.currency_id.id or False,
4364+ 'code': code,
4365+ 'type': account_template.type,
4366+ 'user_type': account_template.user_type and account_template.user_type.id or False,
4367+ 'reconcile': account_template.reconcile,
4368+ 'shortcut': account_template.shortcut,
4369+ 'note': account_template.note,
4370+ 'parent_id': account_template.parent_id and account_template_mapping.get(account_template.parent_id.id) or False,
4371+ 'tax_ids': [(6, 0, tax_ids)],
4372+ 'company_id': wizard.company_id.id,
4373+ }
4374+
4375+ account_id = None
4376+ modified = False
4377+
4378+ if wiz_account.type == 'new':
4379+ #
4380+ # Create the account
4381+ #
4382+ try:
4383+ account_id = account_facade.create(cr, uid, vals)
4384+ log.add(_("Created account %s.\n") % code)
4385+ new_accounts += 1
4386+ modified = True
4387+ except osv.except_osv, ex:
4388+ log.add(_("Exception creating account %s: %s - %s.\n")
4389+ % (code, ex.name, ex.value), True)
4390+ elif wizard.update_account and wiz_account.update_account_id:
4391+ #
4392+ # Update the account
4393+ #
4394+ account_id = wiz_account.update_account_id.id
4395+ try:
4396+ account_facade.write(cr, uid, [account_id], vals)
4397+ log.add(_("Updated account %s.\n") % code)
4398+ updated_accounts += 1
4399+ modified = True
4400+ except osv.except_osv, ex:
4401+ log.add(_("Exception writing account %s: %s - %s.\n")
4402+ % (code, ex.name, ex.value), True)
4403+ else:
4404+ account_id = wiz_account.update_account_id and wiz_account.update_account_id.id
4405+
4406+ # Store the account on the map
4407+ account_template_mapping[account_template.id] = account_id
4408+
4409+ if modified:
4410+ #
4411+ # Detect errors
4412+ #
4413+ if account_template.parent_id and not account_template_mapping.get(account_template.parent_id.id):
4414+ log.add(_("Account %s: The parent account %s can not be set.\n") % (code, account_template.parent_id.code), True)
4415+
4416+ #
4417+ # Set this account as the parent of the accounts that seem to
4418+ # be its children (brothers starting with the same code).
4419+ #
4420+ if wizard.update_children_accounts_parent:
4421+ self._update_children_accounts_parent(
4422+ cr, uid, wizard, log, account_id, context=context)
4423+
4424+ #
4425+ # Reenable the parent_store computing on account_account
4426+ # and force the recomputation.
4427+ #
4428+ self.pool._init = False
4429+ self.pool.get('account.account')._parent_store_compute(cr)
4430+
4431+ return {
4432+ 'new': new_accounts,
4433+ 'updated': updated_accounts,
4434+ 'mapping': account_template_mapping
4435+ }
4436+
4437+ def _update_taxes_pending_for_accounts(self, cr, uid, wizard, log, taxes_pending_for_accounts, account_template_mapping, context=None):
4438+ """
4439+ Updates the taxes (created or updated on previous steps) to set
4440+ the references to the accounts (the taxes where created/updated first,
4441+ when the referenced accounts where still not available).
4442+ """
4443+ tax_facade = self.pool.get('account.tax')
4444+ account_template_facade = self.pool.get('account.account.template')
4445+
4446+ for key, value in taxes_pending_for_accounts.items():
4447+ #
4448+ # Ensure the related account templates are on the map.
4449+ #
4450+ if value['account_collected_id']:
4451+ account_template = account_template_facade.browse(
4452+ cr, uid, value['account_collected_id'], context=context)
4453+ self._map_account_template(cr, uid, wizard, account_template_mapping, account_template, context)
4454+ if value['account_paid_id']:
4455+ account_template = account_template_facade.browse(
4456+ cr, uid, value['account_paid_id'], context=context)
4457+ self._map_account_template(cr, uid, wizard, account_template_mapping, account_template, context)
4458+
4459+ if value['account_collected_id'] or value['account_paid_id']:
4460+ if account_template_mapping.get(value['account_collected_id']) and account_template_mapping.get(value['account_paid_id']):
4461+ vals = {
4462+ 'account_collected_id': account_template_mapping[value['account_collected_id']],
4463+ 'account_paid_id': account_template_mapping[value['account_paid_id']],
4464+ }
4465+ tax_facade.write(cr, uid, [key], vals)
4466+ else:
4467+ tax = tax_facade.browse(cr, uid, key)
4468+ if not account_template_mapping.get(value['account_collected_id']):
4469+ log.add(_("Tax %s: The collected account can not be set.\n") % (tax.name), True)
4470+ if not account_template_mapping.get(value['account_paid_id']):
4471+ log.add(_("Tax %s: The paid account can not be set.\n")
4472+ % (tax.name), True)
4473+
4474+ def _update_fiscal_positions(self, cr, uid, wizard, log, tax_template_mapping, account_template_mapping, context=None):
4475+ """
4476+ Search for, and load, fiscal position templates to create/update.
4477+ """
4478+ fp_facade = self.pool.get('account.fiscal.position')
4479+ fp_tax_facade = self.pool.get('account.fiscal.position.tax')
4480+ fp_account_facade = self.pool.get('account.fiscal.position.account')
4481+
4482+ new_fps = 0
4483+ updated_fps = 0
4484+
4485+ for wiz_fp in wizard.fiscal_position_ids:
4486+ fp_template = wiz_fp.fiscal_position_id
4487+
4488+ fp_id = None
4489+ modified = False
4490+ if wiz_fp.type == 'new':
4491+ #
4492+ # Create a new fiscal position
4493+ #
4494+ vals_fp = {
4495+ 'company_id': wizard.company_id.id,
4496+ 'name': fp_template.name,
4497+ }
4498+ fp_id = fp_facade.create(cr, uid, vals_fp)
4499+ new_fps += 1
4500+ modified = True
4501+ elif wizard.update_fiscal_position and wiz_fp.update_fiscal_position_id:
4502+ #
4503+ # Update the given fiscal position (remove the tax and account
4504+ # mappings, that will be regenerated later)
4505+ #
4506+ fp_id = wiz_fp.update_fiscal_position_id.id
4507+ updated_fps += 1
4508+ modified = True
4509+ # Remove the tax mappings
4510+ fp_tax_ids = fp_tax_facade.search(
4511+ cr, uid, [('position_id', '=', fp_id)])
4512+ fp_tax_facade.unlink(cr, uid, fp_tax_ids)
4513+ # Remove the account mappings
4514+ fp_account_ids = fp_account_facade.search(
4515+ cr, uid, [('position_id', '=', fp_id)])
4516+ fp_account_facade.unlink(cr, uid, fp_account_ids)
4517+ else:
4518+ fp_id = wiz_fp.update_fiscal_position_id and wiz_fp.update_fiscal_position_id.id
4519+
4520+ if modified:
4521+ #
4522+ # (Re)create the tax mappings
4523+ #
4524+ for fp_tax in fp_template.tax_ids:
4525+ #
4526+ # Ensure the related tax templates are on the map.
4527+ #
4528+ self._map_tax_template(cr, uid, wizard, tax_template_mapping, fp_tax.tax_src_id, context)
4529+ if fp_tax.tax_dest_id:
4530+ self._map_tax_template(cr, uid, wizard, tax_template_mapping, fp_tax.tax_dest_id, context)
4531+
4532+ #
4533+ # Create the fp tax mapping
4534+ #
4535+ vals_tax = {
4536+ 'tax_src_id': tax_template_mapping.get(fp_tax.tax_src_id.id),
4537+ 'tax_dest_id': fp_tax.tax_dest_id and tax_template_mapping.get(fp_tax.tax_dest_id.id),
4538+ 'position_id': fp_id,
4539+ }
4540+ fp_tax_facade.create(cr, uid, vals_tax)
4541+
4542+ #
4543+ # Check for errors
4544+ #
4545+ if not tax_template_mapping.get(fp_tax.tax_src_id.id):
4546+ log.add(_("Fiscal position %s: The source tax %s can not be set.\n") % (fp_template.name, fp_tax.tax_src_id.code), True)
4547+ if fp_tax.tax_dest_id and not tax_template_mapping.get(fp_tax.tax_dest_id.id):
4548+ log.add(_("Fiscal position %s: The destination tax %s can not be set.\n") % (fp_template.name, fp_tax.tax_dest_id.name), True)
4549+ #
4550+ # (Re)create the account mappings
4551+ #
4552+ for fp_account in fp_template.account_ids:
4553+ #
4554+ # Ensure the related account templates are on the map.
4555+ #
4556+ self._map_account_template(cr, uid, wizard, account_template_mapping, fp_account.account_src_id, context)
4557+ if fp_account.account_dest_id:
4558+ self._map_account_template(cr, uid, wizard, account_template_mapping, fp_account.account_dest_id, context)
4559+
4560+ #
4561+ # Create the fp account mapping
4562+ #
4563+ vals_account = {
4564+ 'account_src_id': account_template_mapping.get(fp_account.account_src_id.id),
4565+ 'account_dest_id': fp_account.account_dest_id and account_template_mapping.get(fp_account.account_dest_id.id),
4566+ 'position_id': fp_id,
4567+ }
4568+ fp_account_facade.create(cr, uid, vals_account)
4569+
4570+ #
4571+ # Check for errors
4572+ #
4573+ if not account_template_mapping.get(fp_account.account_src_id.id):
4574+ log.add(_("Fiscal position %s: The source account %s can not be set.\n") % (fp_template.name, fp_account.account_src_id.code), True)
4575+ if fp_account.account_dest_id and not account_template_mapping.get(fp_account.account_dest_id.id):
4576+ log.add(_("Fiscal position %s: The destination account %s can not be set.\n") % (fp_template.name, fp_account.account_dest_id.code), True)
4577+
4578+ log.add(_("Created or updated fiscal position %s.\n")
4579+ % fp_template.name)
4580+ return {'new': new_fps, 'updated': updated_fps}
4581+
4582+ def action_update_records(self, cr, uid, ids, context=None):
4583+ """
4584+ Action that creates/updates the selected elements.
4585+ """
4586+ if context is None:
4587+ context = {}
4588+ wizard = self.browse(cr, uid, ids[0], context=context)
4589+
4590+ if wizard.lang:
4591+ context['lang'] = wizard.lang
4592+ elif context.get('lang'):
4593+ del context['lang']
4594+
4595+ log = WizardLog()
4596+
4597+ #
4598+ # Create or update the records.
4599+ #
4600+ tax_codes_res = self._update_tax_codes(
4601+ cr, uid, wizard, log, context=context)
4602+ taxes_res = self._update_taxes(
4603+ cr, uid, wizard, log, tax_codes_res['mapping'], context=context)
4604+ accounts_res = self._update_accounts(
4605+ cr, uid, wizard, log, taxes_res['pending'], context=context)
4606+ self._update_taxes_pending_for_accounts(cr, uid, wizard, log, taxes_res['pending'], accounts_res['mapping'], context=context)
4607+ fps_res = self._update_fiscal_positions(cr, uid, wizard, log, taxes_res['mapping'], accounts_res['mapping'], context=context)
4608+
4609+ #
4610+ # Check if errors where detected and wether we should stop.
4611+ #
4612+ if log.has_errors() and not wizard.continue_on_errors:
4613+ raise osv.except_osv(_('Error'), _(
4614+ "One or more errors detected!\n\n%s") % log.get_errors_str())
4615+
4616+ #
4617+ # Store the data and go to the next step.
4618+ #
4619+ self.write(cr, uid, [wizard.id], {
4620+ 'state': 'done',
4621+ 'new_tax_codes': tax_codes_res.get('new', 0),
4622+ 'new_taxes': taxes_res.get('new', 0),
4623+ 'new_accounts': accounts_res .get('new', 0),
4624+ 'new_fps': fps_res.get('new', 0),
4625+ 'updated_tax_codes': tax_codes_res.get('updated', 0),
4626+ 'updated_taxes': taxes_res.get('updated', 0),
4627+ 'updated_accounts': accounts_res.get('updated', 0),
4628+ 'updated_fps': fps_res.get('updated', 0),
4629+ 'log': log(),
4630+ }, context)
4631+
4632+ return True
4633+
4634+wizard_update_charts_accounts()
4635+
4636+
4637+class wizard_update_charts_accounts_tax_code(osv.osv_memory):
4638+ """
4639+ Tax code that needs to be updated (new or updated in the template).
4640+ """
4641+ _name = 'wizard.update.charts.accounts.tax.code'
4642+
4643+ _columns = {
4644+ 'tax_code_id': fields.many2one('account.tax.code.template', 'Tax code template', required=True),
4645+ 'update_chart_wizard_id': fields.many2one('wizard.update.charts.accounts', 'Update chart wizard', required=True),
4646+ 'type': fields.selection([
4647+ ('new', 'New template'),
4648+ ('updated', 'Updated template'),
4649+ ], 'Type'),
4650+ 'update_tax_code_id': fields.many2one('account.tax.code', 'Tax code to update', required=False),
4651+ 'notes': fields.text('Notes', readonly=True),
4652+ }
4653+
4654+ _defaults = {
4655+ #'update_tax_code_id': lambda *a: None,
4656+ }
4657+
4658+wizard_update_charts_accounts_tax_code()
4659+
4660+
4661+class wizard_update_charts_accounts_tax(osv.osv_memory):
4662+ """
4663+ Tax that needs to be updated (new or updated in the template).
4664+ """
4665+ _name = 'wizard.update.charts.accounts.tax'
4666+
4667+ _columns = {
4668+ 'tax_id': fields.many2one('account.tax.template', 'Tax template', required=True),
4669+ 'update_chart_wizard_id': fields.many2one('wizard.update.charts.accounts', 'Update chart wizard', required=True),
4670+ 'type': fields.selection([
4671+ ('new', 'New template'),
4672+ ('updated', 'Updated template'),
4673+ ], 'Type'),
4674+ 'update_tax_id': fields.many2one('account.tax', 'Tax to update', required=False),
4675+ 'notes': fields.text('Notes', readonly=True),
4676+ }
4677+
4678+ _defaults = {
4679+ #'update_tax_id': lambda *a: None,
4680+ }
4681+
4682+wizard_update_charts_accounts_tax()
4683+
4684+
4685+class wizard_update_charts_accounts_account(osv.osv_memory):
4686+ """
4687+ Account that needs to be updated (new or updated in the template).
4688+ """
4689+ _name = 'wizard.update.charts.accounts.account'
4690+
4691+ # The chart of accounts can have a lot of accounts, so we need an higher
4692+ # limit for the objects in memory to let the wizard create all the items
4693+ # at once.
4694+ _max_count = 4096
4695+
4696+ _columns = {
4697+ 'account_id': fields.many2one('account.account.template', 'Account template', required=True),
4698+ 'update_chart_wizard_id': fields.many2one('wizard.update.charts.accounts', 'Update chart wizard', required=True),
4699+ 'type': fields.selection([
4700+ ('new', 'New template'),
4701+ ('updated', 'Updated template'),
4702+ ], 'Type'),
4703+ 'update_account_id': fields.many2one('account.account', 'Account to update', required=False),
4704+ 'notes': fields.text('Notes', readonly=True),
4705+ }
4706+
4707+ _defaults = {
4708+ #'update_account_id': lambda *a: None,
4709+ }
4710+
4711+wizard_update_charts_accounts_account()
4712+
4713+
4714+class wizard_update_charts_accounts_fiscal_position(osv.osv_memory):
4715+ """
4716+ Fiscal position that needs to be updated (new or updated in the template).
4717+ """
4718+ _name = 'wizard.update.charts.accounts.fiscal.position'
4719+
4720+ _columns = {
4721+ 'fiscal_position_id': fields.many2one('account.fiscal.position.template', 'Fiscal position template', required=True),
4722+ 'update_chart_wizard_id': fields.many2one('wizard.update.charts.accounts', 'Update chart wizard', required=True),
4723+ 'type': fields.selection([
4724+ ('new', 'New template'),
4725+ ('updated', 'Updated template'),
4726+ ], 'Type'),
4727+ 'update_fiscal_position_id': fields.many2one('account.fiscal.position', 'Fiscal position to update', required=False),
4728+ 'notes': fields.text('Notes', readonly=True),
4729+ }
4730+
4731+ _defaults = {
4732+ #'update_fiscal_position_id': lambda *a: None,
4733+ }
4734+
4735+
4736+wizard_update_charts_accounts_fiscal_position()
4737+
4738+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
4739
4740=== added file 'account_chart_update/account_view.xml'
4741--- account_chart_update/account_view.xml 1970-01-01 00:00:00 +0000
4742+++ account_chart_update/account_view.xml 2012-12-07 10:00:37 +0000
4743@@ -0,0 +1,159 @@
4744+<?xml version="1.0" encoding="utf-8"?>
4745+<openerp>
4746+ <data>
4747+
4748+ <!-- Wizard for Multi Charts of Accounts -->
4749+
4750+ <record id="view_update_multi_chart" model="ir.ui.view">
4751+ <field name="name">Update Chart of Accounts from a Chart Template</field>
4752+ <field name="model">wizard.update.charts.accounts</field>
4753+ <field name="type">form</field>
4754+ <field name="arch" type="xml">
4755+ <form string="Update Chart of Accounts from a Chart Template">
4756+ <group col="4" colspan="4" attrs="{'invisible':[('state','!=','init')]}">
4757+ <label string="This wizard will update your accounts, taxes and fiscal positions according to the selected chart template." colspan="4"/>
4758+ <label string="" colspan="4"/>
4759+ <group colspan="4">
4760+ <separator col="4" colspan="4" string="Chart of Accounts"/>
4761+ <field name="company_id" on_change="onchange_company_id(company_id)"/>
4762+ <field name="code_digits"/>
4763+ <field name="chart_template_id"/>
4764+ <field name="lang"/>
4765+ </group>
4766+ <label string=""/>
4767+ <group colspan="4">
4768+ <separator string="Update records?" colspan="4"/>
4769+ <group colspan="2" col="2">
4770+ <field name="update_tax_code"/>
4771+ <field name="update_tax"/>
4772+ <field name="update_account"/>
4773+ <field name="update_fiscal_position"/>
4774+ </group>
4775+ <group colspan="2">
4776+ <label string="If you leave these options set, the wizard will not just create new records, but also update records with changes (i.e. different tax amount)." colspan="4" align="0.0"/>
4777+ <label string="Note: Not all the fields are tested for changes, just the main ones." colspan="4" align="0.0"/>
4778+ </group>
4779+ </group>
4780+ <group colspan="4">
4781+ <separator string="Other options" colspan="4"/>
4782+ <field name="update_children_accounts_parent"/>
4783+ <field name="continue_on_errors"/>
4784+ </group>
4785+ </group>
4786+
4787+ <group col="4" colspan="4" attrs="{'invisible':[('state','!=','ready')]}">
4788+ <separator colspan="4" string="Records to create/update"/>
4789+ <notebook colspan="4">
4790+ <page string="Tax codes">
4791+ <field name="tax_code_ids" colspan="4" nolabel="1" mode="tree,form" height="330">
4792+ <tree string="Tax codes" colors="red:type=='updated'">
4793+ <field name="tax_code_id"/>
4794+ <field name="update_tax_code_id"/>
4795+ <field name="type" invisible="1"/>
4796+ </tree>
4797+ <form string="Tax code">
4798+ <field name="tax_code_id" colspan="4"/>
4799+ <field name="type"/>
4800+ <field name="update_tax_code_id"/>
4801+ <separator string="Notes" colspan="4"/>
4802+ <field name="notes" colspan="4" nolabel="1"/>
4803+ </form>
4804+ </field>
4805+ </page>
4806+ <page string="Taxes">
4807+ <field name="tax_ids" colspan="4" nolabel="1" mode="tree,form" height="330">
4808+ <tree string="Taxes" colors="red:type=='updated'">
4809+ <field name="tax_id"/>
4810+ <field name="update_tax_id"/>
4811+ <field name="type" invisible="1"/>
4812+ </tree>
4813+ <form string="Tax">
4814+ <field name="tax_id" colspan="4"/>
4815+ <field name="type"/>
4816+ <field name="update_tax_id"/>
4817+ <separator string="Notes" colspan="4"/>
4818+ <field name="notes" colspan="4" nolabel="1"/>
4819+ </form>
4820+ </field>
4821+ </page>
4822+ <page string="Accounts">
4823+ <field name="account_ids" colspan="4" nolabel="1" mode="tree,form" height="330">
4824+ <tree string="Accounts" colors="red:type=='updated'">
4825+ <field name="account_id"/>
4826+ <field name="update_account_id"/>
4827+ <field name="type" invisible="1"/>
4828+ </tree>
4829+ <form string="Account">
4830+ <field name="account_id" colspan="4"/>
4831+ <field name="type"/>
4832+ <field name="update_account_id"/>
4833+ <separator string="Notes" colspan="4"/>
4834+ <field name="notes" colspan="4" nolabel="1"/>
4835+ </form>
4836+ </field>
4837+ </page>
4838+ <page string="Fiscal positions">
4839+ <field name="fiscal_position_ids" colspan="4" nolabel="1" mode="tree,form" height="330">
4840+ <tree string="Fiscal positions" colors="red:type=='updated'">
4841+ <field name="fiscal_position_id"/>
4842+ <field name="update_fiscal_position_id"/>
4843+ <field name="type" invisible="1"/>
4844+ </tree>
4845+ <form string="Fiscal position">
4846+ <field name="fiscal_position_id" colspan="4"/>
4847+ <field name="type"/>
4848+ <field name="update_fiscal_position_id"/>
4849+ <separator string="Notes" colspan="4"/>
4850+ <field name="notes" colspan="4" nolabel="1"/>
4851+ </form>
4852+ </field>
4853+ </page>
4854+ </notebook>
4855+ </group>
4856+
4857+ <group col="4" colspan="4" attrs="{'invisible':[('state','!=','done'),]}">
4858+ <separator colspan="4" string="Log"/>
4859+ <field name="log" colspan="4" nolabel="1"/>
4860+ <group colspan="4">
4861+ <separator colspan="4" string="Summary of created objects"/>
4862+ <field name="new_tax_codes"/>
4863+ <field name="new_taxes"/>
4864+ <field name="new_accounts"/>
4865+ <field name="new_fps"/>
4866+ </group>
4867+ <group colspan="4">
4868+ <separator colspan="4" string="Summary of updated objects"/>
4869+ <field name="updated_tax_codes"/>
4870+ <field name="updated_taxes"/>
4871+ <field name="updated_accounts"/>
4872+ <field name="updated_fps"/>
4873+ </group>
4874+ </group>
4875+
4876+ <separator string="" colspan="4"/>
4877+ <group col="8" colspan="4">
4878+ <field name="state"/>
4879+ <button icon="gtk-cancel" special="cancel" string="Cancel" states="init,ready"/>
4880+ <button icon="gtk-go-forward" name="action_find_records" string="Next" type="object" states="init"/>
4881+ <button icon="gtk-go-back" name="action_init" string="Previous" type="object" states="ready"/>
4882+ <button icon="gtk-ok" name="action_update_records" string="Create/Update" type="object" states="ready"/>
4883+ <button icon="gtk-ok" special="cancel" string="Ok" type="object" states="done"/>
4884+ </group>
4885+
4886+ </form>
4887+ </field>
4888+ </record>
4889+
4890+ <record id="action_wizard_update_chart" model="ir.actions.act_window">
4891+ <field name="name">Update Chart of Accounts from a Chart Template</field>
4892+ <field name="type">ir.actions.act_window</field>
4893+ <field name="res_model">wizard.update.charts.accounts</field>
4894+ <field name="view_type">form</field>
4895+ <field name="view_mode">form</field>
4896+ <field name="target">new</field>
4897+ </record>
4898+
4899+ <menuitem parent="account.account_template_folder" action="action_wizard_update_chart" id="menu_wizard"/>
4900+
4901+ </data>
4902+</openerp>
4903
4904=== added directory 'account_chart_update/i18n'
4905=== added file 'account_chart_update/i18n/account_chart_update.pot'
4906--- account_chart_update/i18n/account_chart_update.pot 1970-01-01 00:00:00 +0000
4907+++ account_chart_update/i18n/account_chart_update.pot 2012-12-07 10:00:37 +0000
4908@@ -0,0 +1,744 @@
4909+# Translation of OpenERP Server.
4910+# This file contains the translation of the following modules:
4911+# * account_chart_update
4912+#
4913+msgid ""
4914+msgstr ""
4915+"Project-Id-Version: OpenERP Server 5.0.10\n"
4916+"Report-Msgid-Bugs-To: support@openerp.com\n"
4917+"POT-Creation-Date: 2010-06-10 15:41:54+0000\n"
4918+"PO-Revision-Date: 2010-06-10 15:41:54+0000\n"
4919+"Last-Translator: <>\n"
4920+"Language-Team: \n"
4921+"MIME-Version: 1.0\n"
4922+"Content-Type: text/plain; charset=UTF-8\n"
4923+"Content-Transfer-Encoding: \n"
4924+"Plural-Forms: \n"
4925+
4926+#. module: account_chart_update
4927+#: field:wizard.update.charts.accounts,lang:0
4928+msgid "Language"
4929+msgstr ""
4930+
4931+#. module: account_chart_update
4932+#: code:addons/account_chart_update/account.py:0
4933+#, python-format
4934+msgid "Created or updated fiscal position %s.\n"
4935+msgstr ""
4936+
4937+#. module: account_chart_update
4938+#: code:addons/account_chart_update/account.py:0
4939+#, python-format
4940+msgid "Exception creating account %s: %s - %s.\n"
4941+msgstr ""
4942+
4943+#. module: account_chart_update
4944+#: code:addons/account_chart_update/account.py:0
4945+#, python-format
4946+msgid "Exception writing account %s: %s - %s.\n"
4947+msgstr ""
4948+
4949+#. module: account_chart_update
4950+#: code:addons/account_chart_update/account.py:0
4951+#, python-format
4952+msgid "Exception setting the parent of account %s children: %s - %s.\n"
4953+msgstr ""
4954+
4955+#. module: account_chart_update
4956+#: code:addons/account_chart_update/account.py:0
4957+#, python-format
4958+msgid "Updated account %s.\n"
4959+msgstr ""
4960+
4961+#. module: account_chart_update
4962+#: code:addons/account_chart_update/account.py:0
4963+#, python-format
4964+msgid "Created account %s.\n"
4965+msgstr ""
4966+
4967+#. module: account_chart_update
4968+#: code:addons/account_chart_update/account.py:0
4969+#, python-format
4970+msgid "Created tax %s.\n"
4971+msgstr ""
4972+
4973+#. module: account_chart_update
4974+#: code:addons/account_chart_update/account.py:0
4975+#, python-format
4976+msgid "Updated tax %s.\n"
4977+msgstr ""
4978+
4979+#. module: account_chart_update
4980+#: code:addons/account_chart_update/account.py:0
4981+#, python-format
4982+msgid "Created tax code %s.\n"
4983+msgstr ""
4984+
4985+#. module: account_chart_update
4986+#: code:addons/account_chart_update/account.py:0
4987+#, python-format
4988+msgid "Updated tax code %s.\n"
4989+msgstr ""
4990+
4991+#. module: account_chart_update
4992+#: code:addons/account_chart_update/account.py:0
4993+#, python-format
4994+msgid "The template has accounts the fiscal position instance does not.\n"
4995+msgstr ""
4996+
4997+#. module: account_chart_update
4998+#: code:addons/account_chart_update/account.py:0
4999+#, python-format
5000+msgid "The base sign field is different.\n"
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches