Revert "[IMP] web: Export list view as XLS file"

This reverts commit ac00548b28.
This commit is contained in:
Lucas Lefèvre
2019-09-19 14:00:49 +02:00
parent 02599afd49
commit 2849b5cb39
8 changed files with 1 additions and 460 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main, pivot, list_controller
from . import main, pivot
-98
View File
@@ -1,98 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from datetime import datetime
from odoo import http
from odoo.http import request
from odoo.tools.misc import xlwt
HEADER_BOLD = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
class TableListExporter(http.Controller):
def _write_group_header(self, worksheet, x, y, group, columns, group_depth=0):
aggregates = group.get('aggregateValues', {})
title = group.get('value')
if group.get('hideHeader'):
return x, y
title = '%s%s (%s)' % ('>' * group_depth, title, group.get('count', 0))
worksheet.write(x, y, title, HEADER_BOLD)
for column in columns[1:]: # No aggregates allowed in the first column
y += 1
worksheet.write(x, y, aggregates.get(column['field'], ''), HEADER_BOLD)
return x + 1, 0
def _write_record(self, worksheet, x, y, record, columns):
for column in columns:
worksheet.write(x, y, record[column['field']])
y += 1
return x + 1, 0
def _write_groups(self, worksheet, x, y, groups, columns, group_depth=0):
"""
Write groups to the xls worksheet. Recursive if groups are nested (chained groupby)
:param worksheet:
:param x: start position where to write the group (vertical axis, pointing downward)
:param y: start position where to write the group (horizontal axis, pointing to the right)
:param groups: list of groups. Each group is a dict containing either records' data
or sub-groups. A group has the following structure:
'data': list of records or list of sub-groups
'isGrouped': True if data contains sub-groups
'value': title of the group
'count': number of records in the group
'aggregateValues': dict container aggregated values of fields {field: aggregatedValue, ...}
'hideHeader': self explanatory, used when the records are not grouped at all.
:param columns: list of displayed columns. Each column is a dict with the following keys:
'field': field name
'aggregateValue': aggregate for all records
'string':
"""
for group in groups:
x, y = self._write_group_header(worksheet, x, y, group, columns, group_depth)
if group['isGrouped']:
# Recursively write sub-groups
x, y = self._write_groups(worksheet, x, y, group['data'], columns, group_depth + 1)
else:
for record in group['data']:
x, y = self._write_record(worksheet, x, y, record, columns)
return x, y
def _write_worksheet(self, worksheet, data):
bold = xlwt.easyxf("font: bold on;")
columns = data['columns']
groups = data['groups']
# Write main header
for y, field in enumerate(columns):
worksheet.write(0, y, field['string'], bold)
# Write data
x, y = 1, 0
x, y = self._write_groups(worksheet, x, y, groups, columns)
# Write column aggregates
for y, field in enumerate(columns):
worksheet.write(x, y, field.get('aggregateValue', ''), bold)
@http.route('/web/list/export_xls', type='http', auth="user")
def export_xls(self, data, token):
data = json.loads(data)
title = data.get('title', 'export')
workbook = xlwt.Workbook()
worksheet = workbook.add_sheet(title)
self._write_worksheet(worksheet, data)
response = request.make_response(None,
headers=[('Content-Type', 'application/vnd.ms-excel'),
('Content-Disposition', 'attachment; filename="%s - %s.xls"' % (title, datetime.now()))],
cookies={'fileToken': token})
workbook.save(response.stream)
return response
@@ -8,78 +8,20 @@ odoo.define('web.ListController', function (require) {
*/
var core = require('web.core');
var session = require('web.session');
var framework = require('web.framework');
var BasicController = require('web.BasicController');
var DataExport = require('web.DataExport');
var Dialog = require('web.Dialog');
var fieldUtils = require('web.field_utils');
var Sidebar = require('web.Sidebar');
var _t = core._t;
var qweb = core.qweb;
/**
* Return data required for exporting the current list to a xls file.
* Recursive if some groups are nested.
* Note: this function is also used when the records are not grouped.
* In this scenario, an artificial single group with every record in
* the list is built.
* @param {Array} groups
* @param {Array} displayedFields
* @param {Array} allFields
* @returns {Array} list of groups with the following structure:
* 'data': list of records or list of sub-groups
* 'isGrouped': true if data contains sub-groups
* 'value': title of the group
* 'count': number of records in the group
* 'aggregateValues': dict container aggregated values of fields {field: aggregatedValue, ...}
* 'hideHeader': self explanatory, used when the records are not grouped at all.
*/
var processGroups = (groups, displayedFields, allFields) => {
let formatRecord = record => {
let data = _.pick(record.data, displayedFields)
let formattedData = {}
Object.entries(data).forEach(([field_name, value]) => {
let field = allFields[field_name]
let formattedValue = fieldUtils.format[field.type](value, field, {data: record.data, forceString: true})
formattedData[field_name] = formattedValue.replace(' ', '')
});
return formattedData
}
if (groups.length && groups[0].groupedBy && groups[0].groupedBy.length) {
// Recursively process sub-group
return groups.map(group => ({
hideHeader: !!group.hideHeader,
isGrouped: true,
value: group.value !== undefined && group.value.toString() || _t("Undefined"),
count: group.count,
aggregateValues: _.pick(group.aggregateValues, displayedFields),
data: processGroups(group.data, displayedFields, allFields),
}))
}
// process records
return groups.map(group => ({
hideHeader: !!group.hideHeader,
isGrouped: false,
value: group.value !== undefined && group.value.toString() || _t("Undefined"),
count: group.count,
aggregateValues: _.pick(group.aggregateValues, displayedFields),
data: group.data.map(formatRecord)
}))
}
var ListController = BasicController.extend({
/**
* This key contains the name of the buttons template to render on top of
* the list view. It can be overridden to add buttons in specific child views.
*/
buttons_template: 'ListView.buttons',
events: _.extend({}, BasicController.prototype.events, {
'click .o_list_download': '_onExportRecords',
}),
custom_events: _.extend({}, BasicController.prototype.custom_events, {
activate_next_widget: '_onActivateNextWidget',
add_record: '_onAddRecord',
@@ -109,9 +51,6 @@ var ListController = BasicController.extend({
this.selectedRecords = params.selectedRecords || [];
this.multipleRecordsSavingPromise = null;
this.fieldChangedPrevented = false;
this.activeActions = _.extend({}, this.activeActions, {
download: this.renderer.arch.attrs.download ? !!JSON.parse(this.renderer.arch.attrs.download) : true,
});
},
//--------------------------------------------------------------------------
@@ -401,31 +340,6 @@ var ListController = BasicController.extend({
self._updateButtons('readonly');
});
},
/**
* Export the current list data in a xls file.
*
* @private
*/
_downloadList() {
let groups = this.renderer.state.data
groups = this.renderer.isGrouped ? groups : [{data: groups, count: groups.length, hideHeader: true}] // Artificial single group
let allFields = this.renderer.state.fields
let columns = this.renderer.columns.map(column => ({
field: column.attrs.name,
aggregateValue: column.aggregate && column.aggregate.value,
string: allFields[column.attrs.name].string,
}))
groups = processGroups(groups, columns.map(c => c.field), allFields)
return session.get_file({
url: '/web/list/export_xls',
data: {data: JSON.stringify({columns, groups, title: this._title})},
complete: framework.unblockUI,
error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
/**
* @override
* @private
@@ -732,14 +646,6 @@ var ListController = BasicController.extend({
});
new DataExport(this, record, defaultExportFields).open();
},
/**
* Export Records in a xls file
*
* @private
*/
_onExportRecords() {
this._downloadList()
},
/**
* Opens the related form view.
*
-3
View File
@@ -275,9 +275,6 @@
Create
</button>
</t>
<t t-if="widget.is_action_enabled('download')">
<button type="button" class="btn btn-secondary fa fa-download o_list_download" title="Download xls" aria-label="Download xls"/>
</t>
</div>
</t>
-187
View File
@@ -3544,193 +3544,6 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('export list view to xls with nested groups', async function (assert) {
assert.expect(5);
let currencies = {};
_.each(this.data.res_currency.records, function (currency) {
currencies[currency.id] = currency;
});
let list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree download="1">
<field name="foo"/>
<field name="bar"/>
<field name="int_field" sum="Total"/>
<field name="datetime"/>
<field name="amount" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="o2m"/>
<field name="m2o"/>
<field name="m2m"/>
</tree>`,
groupBy: ['foo', 'bar'],
session: {
currencies: currencies,
get_file(args) {
let data = JSON.parse(args.data.data);
assert.strictEqual(args.url, '/web/list/export_xls',
"should call get_file with the correct url");
assert.deepEqual(data.columns, [
{field: "foo", string: "Foo"},
{field: "bar", string: "Bar"},
{field: "int_field", aggregateValue: 32, string: "int_field"},
{field: "datetime", string: "Datetime Field"},
{field: "amount", string: "Monetary field"},
{field: "o2m", string: "O2M field"},
{field: "m2o", string: "M2O field"},
{field: "m2m", string: "M2M field"},
], "columns should be in the correct order with the string and aggregate value")
assert.deepEqual(data.groups[0], {
// Group "yop"
isGrouped: true,
count: 1,
aggregateValues: {int_field: 10},
hideHeader: false,
value: "yop",
data: [{
// Group "yop > true"
isGrouped: false,
count: 1,
aggregateValues: {int_field: 10},
hideHeader: false,
value: "true",
data: [{
// Record "yop > true"
foo: "yop",
bar: "True",
int_field: "10",
datetime: "12/12/2016 10:55:05",
amount: "1200.00€",
m2m: "2 records",
m2o: "Value 1",
o2m: "No records",
}]
}]
});
assert.deepEqual(data.groups[1], {
// Group "blip"
isGrouped: true,
count: 2,
aggregateValues: {int_field: 5},
hideHeader: false,
value: "blip",
data: [{
// Group "blip > true"
isGrouped: false,
count: 1,
aggregateValues: {int_field: 9},
hideHeader: false,
value: "true",
data: [],
}, {
// Group "blip > false"
isGrouped: false,
count: 1,
aggregateValues: {int_field: -4},
hideHeader: false,
value: "false",
data: [],
}]
});
assert.deepEqual(data.groups[2], {
// Group "gnap"
isGrouped: true,
count: 1,
aggregateValues: {int_field: 17},
hideHeader: false,
value: "gnap",
data: [],
});
args.complete();
},
},
});
// open the first group
await testUtils.dom.click(list.$('.o_group_header:first'));
// open subgroup
let $openGroup = list.$('tbody:nth(1)');
await testUtils.dom.click($openGroup.find('.o_group_header:first'));
// open the second group
await testUtils.dom.click(list.$('.o_group_header:nth(2)'));
// Download
await testUtils.dom.click(list.$buttons.find('.o_list_download'));
list.destroy();
});
QUnit.test('export list view to xls', async function (assert) {
assert.expect(3);
let currencies = {};
_.each(this.data.res_currency.records, function (currency) {
currencies[currency.id] = currency;
});
let list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
<field name="int_field" sum="Total"/>
<field name="datetime"/>
<field name="amount" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="o2m"/>
<field name="m2o"/>
<field name="m2m"/>
</tree>`,
session: {
currencies: currencies,
get_file(args) {
let data = JSON.parse(args.data.data);
assert.strictEqual(args.url, '/web/list/export_xls',
"should call get_file with the correct url");
assert.deepEqual(data.columns, [
{field: "foo", string: "Foo"},
{field: "bar", string: "Bar"},
{field: "int_field", aggregateValue: 32, string: "int_field"},
{field: "datetime", string: "Datetime Field"},
{field: "amount", string: "Monetary field"},
{field: "o2m", string: "O2M field"},
{field: "m2o", string: "M2O field"},
{field: "m2m", string: "M2M field"},
], "columns should be in the correct order with the string and aggregate value")
assert.deepEqual(data.groups[0], {
isGrouped: false,
count: 4,
aggregateValues: {},
hideHeader: true,
value: "Undefined",
data: [
{foo: "yop", bar: "True", int_field: "10", datetime: "12/12/2016 10:55:05", amount: "1200.00€", m2m: "2 records", m2o: "Value 1", o2m: "No records"},
{foo: "blip", bar: "True", int_field: "9", datetime: "", amount: "$500.00", m2m: "3 records", m2o: "Value 2", o2m: "No records"},
{foo: "gnap", bar: "True", int_field: "17", datetime: "", amount: "$300.00", m2m: "No records", m2o: "Value 1", o2m: "No records"},
{foo: "blip", bar: "False", int_field: "-4", datetime: "", amount: "$0.00", m2m: "1 record", m2o: "Value 1", o2m: "No records"},
],
});
args.complete();
},
},
});
// Download
await testUtils.dom.click(list.$buttons.find('.o_list_download'));
list.destroy();
});
QUnit.test('grouped list on selection field at level 2', async function (assert) {
assert.expect(4);
-1
View File
@@ -6,4 +6,3 @@ from . import test_js
from . import test_menu
from . import test_serving_base
from . import test_click_everywhere
from . import test_list_controller
-75
View File
@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ..controllers.list_controller import TableListExporter
from odoo.tests.common import HttpCase
class WorksheetMock(dict):
def write(self, x, y, value, style=None):
if (x, y) in self:
raise Exception("Cannot rewrite cell [{}, {}]".format(x, y))
self[x, y] = value
class ListController(HttpCase):
def test_list_download(self):
controller = TableListExporter()
columns = [
{'field': "foo", 'string': "Foo"},
{'field': "bar", 'string': "Bar"},
{'field': "int_field", 'aggregateValue': 19, 'string': "int_field"},
]
groups = [{
# Group "yop"
'isGrouped': True,
'count': 1,
'aggregateValues': {'int_field': 19},
'hideHeader': False,
'value': "yop",
'data': [{
# Group "yop > true"
'isGrouped': False,
'count': 1,
'aggregateValues': {'int_field': 10},
'hideHeader': False,
'value': "true",
'data': [{'foo': "yop", 'bar': "True", 'int_field': 10}],
}, {
# Group "yop > false"
'isGrouped': False,
'count': 10,
'aggregateValues': {'int_field': 9},
'hideHeader': False,
'value': "false",
'data': [],
}],
}]
worksheet = WorksheetMock()
controller._write_worksheet(worksheet, {'columns': columns, 'groups': groups})
# Header
self.assertEqual(worksheet[0, 0], 'Foo')
self.assertEqual(worksheet[0, 1], 'Bar')
self.assertEqual(worksheet[0, 2], 'int_field')
# Main group
self.assertEqual(worksheet[1, 0], 'yop (1)', "Group header")
self.assertEqual(worksheet[1, 2], 19, "int_field aggregate value")
# First sub-group (open)
self.assertEqual(worksheet[2, 0], '>true (1)', "subgroup header")
self.assertEqual(worksheet[2, 2], 10, "int_field aggregate value")
# data
self.assertEqual(worksheet[3, 0], 'yop')
self.assertEqual(worksheet[3, 1], 'True')
self.assertEqual(worksheet[3, 2], 10)
# second sub-group (closed)
self.assertEqual(worksheet[4, 0], '>false (10)', "second sub-group header")
self.assertEqual(worksheet[4, 2], 9, "int_field aggregate value")
self.assertEqual(worksheet[5, 2], 19, "main list int_field aggregate value")
-1
View File
@@ -27,7 +27,6 @@
<rng:optional><rng:attribute name="create"/></rng:optional>
<rng:optional><rng:attribute name="delete"/></rng:optional>
<rng:optional><rng:attribute name="edit"/></rng:optional>
<rng:optional><rng:attribute name="download"/></rng:optional>
<rng:optional><rng:attribute name="duplicate"/></rng:optional>
<rng:optional><rng:attribute name="import"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional> <!-- deprecated, has no effect anymore -->