From a60dd3efff0661c7d84f39292ed2c1a1c6837a06 Mon Sep 17 00:00:00 2001 From: Damien Crier Date: Thu, 28 Apr 2016 20:03:20 +0200 Subject: [PATCH] [IMP] export general ledger to XLSX format --- account_financial_report_qweb/__openerp__.py | 1 + .../report/__init__.py | 1 + .../report/general_ledger_xlsx.py | 95 ++++++++++ account_financial_report_qweb/reports.xml | 9 + .../wizard/general_ledger_wizard.xml | 2 + .../wizard/ledger_report_wizard.py | 172 ++++++++++++++++++ 6 files changed, 280 insertions(+) create mode 100644 account_financial_report_qweb/report/general_ledger_xlsx.py diff --git a/account_financial_report_qweb/__openerp__.py b/account_financial_report_qweb/__openerp__.py index e65be44d..33b23c0f 100644 --- a/account_financial_report_qweb/__openerp__.py +++ b/account_financial_report_qweb/__openerp__.py @@ -17,6 +17,7 @@ 'account_full_reconcile', 'date_range', 'account_fiscal_year', + 'report_xlsx', ], 'data': [ 'wizard/aged_partner_balance_wizard_view.xml', diff --git a/account_financial_report_qweb/report/__init__.py b/account_financial_report_qweb/report/__init__.py index 329caec3..25a4298d 100644 --- a/account_financial_report_qweb/report/__init__.py +++ b/account_financial_report_qweb/report/__init__.py @@ -5,3 +5,4 @@ from . import common from . import general_ledger from . import open_invoice +from . import general_ledger_xlsx diff --git a/account_financial_report_qweb/report/general_ledger_xlsx.py b/account_financial_report_qweb/report/general_ledger_xlsx.py new file mode 100644 index 00000000..9247e497 --- /dev/null +++ b/account_financial_report_qweb/report/general_ledger_xlsx.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Author: Damien Crier +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx +from openerp.report import report_sxw + +from ..wizard.ledger_report_wizard import FIELDS_TO_READ + +_logger = logging.getLogger(__name__) + +FIELDS_TO_READ_INV = {v: k for k, v in FIELDS_TO_READ.iteritems()} + + +class GeneralLedgerXslx(ReportXlsx): + + def __init__(self, name, table, rml=False, parser=False, header=True, + store=False): + super(GeneralLedgerXslx, self).__init__( + name, table, rml, parser, header, store) + + def generate_xlsx_report(self, workbook, data, objects): + + data = objects.compute() + report_name = data['header'][0].get('title', 'GeneralLedgerXslx') + sheet = workbook.add_worksheet(report_name[:31]) + row_pos = 0 + col_pos = 0 + + sheet.set_column(col_pos, col_pos, 30) + bold = workbook.add_format({'bold': True}) + header_format = workbook.add_format({'bold': True, + 'align': 'center', + 'border': True, + 'bg_color': '#FFFFCC'}) + std_cell_format = workbook.add_format({'bold': False, + 'align': 'left', + 'border': False, + 'bg_color': '#FFFFFF'}) + # write report title on first cell A0 in XLSX sheet + sheet.write(row_pos, 0, report_name, bold) + row_pos += 2 + + # write filters taken for this report + col = 1 + for k, v in data['header'][0].get('filters').iteritems(): + sheet.write(row_pos, col, k, header_format) + sheet.write(row_pos+1, col, v, header_format) + col += 2 + + row_pos += 5 + + # write header line + col = 0 + content = data['content'][0] + + header_done = False + sorted_acc = content.keys() + sorted_acc.sort() + for acc in sorted_acc: + for move_lines in content[acc]: + # write line with account name + # sheet.write(row_pos, col+1, acc, bold) + # row_pos += 1 + # write column headers + if not header_done: + for k, v in move_lines.iteritems(): + position = FIELDS_TO_READ_INV.get(k, 'remove') + if position == 'remove': + continue + sheet.write(row_pos, position, k, bold) + # col2 += 1 + row_pos += 1 + header_done = True + + for k, v in move_lines.iteritems(): + if isinstance(v, (list, tuple)): + v = v[1] + position = FIELDS_TO_READ_INV.get(k, 'remove') + if position == 'remove': + continue + if position == 0: + sheet.write(row_pos, position, v, bold) + else: + sheet.write(row_pos, position, v, std_cell_format) + + # col2 += 1 + row_pos += 1 + + +GeneralLedgerXslx('report.ledger.report.wizard.xlsx', + 'ledger.report.wizard', parser=report_sxw.rml_parse) diff --git a/account_financial_report_qweb/reports.xml b/account_financial_report_qweb/reports.xml index e56ae668..08a2ae2d 100644 --- a/account_financial_report_qweb/reports.xml +++ b/account_financial_report_qweb/reports.xml @@ -36,5 +36,14 @@ + + General Ledger XLSX report + ledger.report.wizard + ir.actions.report.xml + ledger.report.wizard.xlsx + xlsx + + + diff --git a/account_financial_report_qweb/wizard/general_ledger_wizard.xml b/account_financial_report_qweb/wizard/general_ledger_wizard.xml index b87159bc..9172a168 100644 --- a/account_financial_report_qweb/wizard/general_ledger_wizard.xml +++ b/account_financial_report_qweb/wizard/general_ledger_wizard.xml @@ -33,6 +33,8 @@ diff --git a/account_financial_report_qweb/wizard/ledger_report_wizard.py b/account_financial_report_qweb/wizard/ledger_report_wizard.py index 035d7110..50e3c200 100644 --- a/account_financial_report_qweb/wizard/ledger_report_wizard.py +++ b/account_financial_report_qweb/wizard/ledger_report_wizard.py @@ -2,8 +2,29 @@ # Author: Damien Crier # Copyright 2016 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from operator import itemgetter + from openerp import models, fields, api, _ +# order to be placed on the report: field +FIELDS_TO_READ = {1: 'date', + 0: 'account_id', + 4: 'account_code', + 2: 'move_name', + 3: 'journal_id', + 5: 'partner_name', + 6: 'ref', + 7: 'label', + 8: 'debit', + 9: 'credit', + 30: 'amount_currency', + 40: 'currency_code', + 50: 'month', + 60: 'partner_ref', + 10: 'cumul_balance', + 70: 'init_balance', + } + class LedgerReportWizard(models.TransientModel): """Base ledger report wizard.""" @@ -215,3 +236,154 @@ class LedgerReportWizardLine(models.TransientModel): invoice_number = fields.Char() centralized = fields.Boolean() + + @api.multi + def check_report_xlsx(self): + self.ensure_one() + data = {} + data['ids'] = self.env.context.get('active_ids', []) + # data['model'] = 'general.ledger.line' + data['model'] = self.env.context.get('active_model', 'ir.ui.menu') + data['form'] = self.read(['date_from', 'date_to', + 'journal_ids', 'target_move'])[0] + used_context = self._build_contexts(data) + data['form']['used_context'] = dict( + used_context, + lang=self.env.context.get('lang', 'en_US')) + return self._print_report_xlsx(data) + + @api.multi + def _print_report_xlsx(self, data): + return { + 'name': 'export xlsx general ledger', + 'model': 'ledger.report.wizard', + 'type': 'ir.actions.report.xml', + 'report_name': 'ledger.report.wizard.xlsx', + 'report_type': 'xlsx', + 'context': self.env.context, + } + + @api.multi + def _get_centralized_move_ids(self, domain): + """ Get last line of each selected centralized accounts """ + # inverse search on centralized boolean to finish the search to get the + # ids of last lines of centralized accounts + # XXX USE DISTINCT to speed up ? + domain = domain[:] + centralize_index = domain.index(('centralized', '=', False)) + domain[centralize_index] = ('centralized', '=', True) + + gl_lines = self.env['general.ledger.line'].search(domain) + accounts = gl_lines.mapped('account_id') + + line_ids = [] + for acc in accounts: + acc_lines = gl_lines.filtered(lambda rec: rec.account_id == acc) + line_ids.append(acc_lines[-1].id) + return line_ids + + @api.multi + def _get_moves_from_dates_domain(self): + """ Prepare domain for `_get_moves_from_dates` """ + domain = [] + if self.centralize: + domain = [('centralized', '=', False)] + start_date = self.date_from + end_date = self.date_to + if start_date: + domain += [('date', '>=', start_date)] + if end_date: + domain += [('date', '<=', end_date)] + + if self.target_move == 'posted': + domain += [('move_state', '=', 'posted')] + + if self.account_ids: + domain += [('account_id', 'in', self.account_ids.ids)] + + return domain + + def compute_domain(self): + ret = self._get_moves_from_dates_domain() + if self.centralize: + centralized_ids = self._get_centralized_move_ids(ret) + if centralized_ids: + ret.insert(0, '|') + ret.append(('id', 'in', centralized_ids)) + return ret + + def initial_balance_line(self, amount, account_name, account_code, date): + return {'date': date, + 'account_id': account_name, + 'account_code': account_code, + 'move_name': '', + 'journal_id': '', + 'partner_name': '', + 'ref': '', + 'label': _('Initial Balance'), + 'debit': '', + 'credit': '', + 'amount_currency': '', + 'currency_code': '', + 'month': '', + 'partner_ref': '', + 'cumul_balance': amount, + 'init_balance': ''} + + def group_general_ledger(self, report_lines, date_start): + """ + group lines by account and order by account then date + """ + result = {} + accounts = report_lines.mapped('account_id') + for account in accounts: + lines = report_lines.filtered( + lambda a: a.account_id.id == account.id) + acc_full_name = account.name_get()[0][1] + sorted_lines = sorted(lines.read(FIELDS_TO_READ.values()), + key=itemgetter('date')) + initial_balance = sorted_lines[0]['init_balance'] + sorted_lines.insert(0, self.initial_balance_line(initial_balance, + acc_full_name, + account.code, + date_start)) + result[acc_full_name] = sorted_lines + + return result + + def construct_header(self): + result = {} + + result['title'] = _('General Ledger') + filters = {} + + filters['centralized'] = _('%s' % self.centralize) + filters['start_date'] = self.date_from + filters['end_date'] = self.date_to + + filters['target_moves'] = self.target_move + + filters['accounts'] = _('All') + if self.account_ids: + filters['accounts'] = ', '.join([a.code for a in self.account_ids]) + + result['filters'] = filters + + return result + + @api.multi + def compute(self): + self.ensure_one() + # header filled with a dict + header = [] + header.append(self.construct_header()) + # content filled with dicts + content = [] + + domain = self.compute_domain() + report_lines = self.env['general.ledger.line'].search(domain) + lines_general_ledger = self.group_general_ledger(report_lines, + self.date_from) + content.append(lines_general_ledger) + return {'header': header, + 'content': content}