Merge PR #611 into 14.0

Signed-off-by thomaspaulb
This commit is contained in:
OCA-git-bot
2022-10-21 12:26:46 +00:00
17 changed files with 544 additions and 11 deletions

View File

@@ -70,6 +70,15 @@ As normal user, you can run your reports from Report Center
- **Files:** show all files being produced by the job as run by the user.
- **Jobs:** show all jobs triggered by this report as run by the user. Only job queue manager have access to this button.
As additional improvement, you can now generate reports async directly from the form view itself,
and not have to go into the "Report Center" specifically to do it. This can be done by pressing
*Print*, you will get a popup asking you if you want to run it Async, and then to verify the email
address that it should be sent to. See below sample:
.. image:: https://raw.githubusercontent.com/OCA/reporting-engine/14.0/report_async/static/description/sample.gif
:width: 800
:alt: How It Works
Bug Tracker
===========
@@ -97,6 +106,11 @@ Contributors
* Saran Lim. <saranl@ecosoft.co.th>
* Tharathip Chaweewongphan <tharathipc@ecosoft.co.th>
* `Sunflower IT <https://sunflowerweb.nl>`__:
* Tom Blauwendraat <tom@sunflowerweb.nl>
* Kevin Kamau <kevin@sunflowerweb.nl>
Maintainers
~~~~~~~~~~~

View File

@@ -12,11 +12,14 @@
"data": [
"security/ir.model.access.csv",
"security/ir_rule.xml",
"views/assets.xml",
"data/mail_template.xml",
"data/queue_job_function_data.xml",
"views/report_async.xml",
"wizard/print_report_wizard.xml",
"views/ir_actions_report.xml",
],
"qweb": ["static/src/xml/report_async.xml"],
"demo": ["demo/report_async_demo.xml"],
"installable": True,
"maintainers": ["kittiu"],

View File

@@ -40,7 +40,8 @@
valign="top"
style="font-size: 13px;"
>
% set base_url = object.env['ir.config_parameter'].sudo().get_param('web.base.url')
% set base_url_async = object.env['ir.config_parameter'].sudo().get_param('web.base.url.async_reports')
% set base_url = base_url_async or object.env['ir.config_parameter'].sudo().get_param('web.base.url')
% set download_url = '%s/web/content/ir.attachment/%s/datas/%s?download=true' % (base_url, object.id, object.name, )
<div>
Dear ${object.create_uid.partner_id.name or ''},

View File

@@ -4,3 +4,4 @@
from . import report_async
from . import ir_report
from . import ir_actions
from . import queue_job

View File

@@ -1,7 +1,7 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import models
from odoo import fields, models
# Define all supported report_type
REPORT_TYPES = ["qweb-pdf", "qweb-text", "qweb-xml", "csv", "excel", "xlsx"]
@@ -10,6 +10,18 @@ REPORT_TYPES = ["qweb-pdf", "qweb-text", "qweb-xml", "csv", "excel", "xlsx"]
class Report(models.Model):
_inherit = "ir.actions.report"
async_report = fields.Boolean(default=False)
async_no_records = fields.Integer(
string="Min of Records",
default=100,
help="Min no of records to use async report functionality; e.g 100+",
)
async_mail_recipient = fields.Char(
string="Mail Recipient",
help="The email that will receive the async report",
default=lambda self: self.env.user.email,
)
def report_action(self, docids, data=None, config=True):
res = super(Report, self).report_action(docids, data=data, config=config)
if res["context"].get("async_process", False):

View File

@@ -0,0 +1,30 @@
# Copyright 2022 Sunflower IT (https://sunflowerweb.nl/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import api, models
class QueueJob(models.Model):
_inherit = "queue.job"
@api.model
def create(self, values):
res = super(QueueJob, self).create(values)
if (
"model_name" in values
and values["model_name"] == "report.async"
and "kwargs" in values
and "to_email" in values["kwargs"]
):
followers = self._find_partner(res, values["kwargs"]["to_email"])
if followers:
res.message_subscribe(partner_ids=followers)
return res
def _find_partner(self, record, email):
partner = self.env["res.partner"].search([("email", "=", email)], limit=1)
followers = record.message_follower_ids.mapped("partner_id")
ids = [x for x in partner.ids if x not in followers.ids]
if partner and ids:
return ids
return None

View File

@@ -2,11 +2,17 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import base64
import logging
import mock
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
# Define all supported report_type
REPORT_TYPES_FUNC = {
"qweb-pdf": "_render_qweb_pdf",
@@ -137,13 +143,56 @@ class ReportAsync(models.Model):
return result
@api.model
def run_report(self, docids, data, report_id, user_id):
def print_document_async(
self,
record_ids,
report_name,
html=None,
data=None,
to_email="",
save_attachment_to_records=False,
):
"""Generate a document async, do not return the document file"""
user_email = to_email or self.env.user.email
report = self.env["ir.actions.report"]._get_report_from_name(report_name)
self.with_delay().run_report(
record_ids,
data or {},
report.id,
self._uid,
email_notify=True,
to_email=user_email,
session_id=request.session.sid,
save_attachment_to_records=save_attachment_to_records,
)
@api.model
def run_report(
self,
docids,
data,
report_id,
user_id,
email_notify=False,
to_email=None,
session_id=None,
save_attachment_to_records=False,
):
report = self.env["ir.actions.report"].browse(report_id)
func = REPORT_TYPES_FUNC[report.report_type]
# Run report
if user_id:
report = report.with_user(user_id)
if session_id:
# necessary for correct CSS headers
with mock.patch("odoo.http.request.session") as session:
session.sid = session_id
out_file, file_ext = getattr(report, func)(docids, data)
else:
out_file, file_ext = getattr(report, func)(docids, data)
# Run report
out_file = base64.b64encode(out_file)
out_name = "{}.{}".format(report.name, file_ext)
_logger.info("ASYNC GENERATION OF REPORT %s", (out_name,))
# Save report to attachment
attachment = (
self.env["ir.attachment"]
@@ -158,6 +207,26 @@ class ReportAsync(models.Model):
}
)
)
# save attachment to records
if save_attachment_to_records:
model = report.model
records = self.env[model].sudo().browse(docids)
for record in records:
attachment = (
self.env["ir.attachment"]
.sudo()
.create(
{
"name": out_name,
"datas": out_file,
"type": "binary",
"res_model": model,
"res_id": record.id,
}
)
)
if hasattr(record, "message_post"):
record.message_post(attachment_ids=[attachment.id])
self._cr.execute(
"""
UPDATE ir_attachment SET create_uid = %s, write_uid = %s
@@ -165,11 +234,20 @@ class ReportAsync(models.Model):
(self._uid, self._uid, attachment.id),
)
# Send email
if self.email_notify:
self._send_email(attachment)
if email_notify or self.email_notify:
self._send_email(attachment, to_email=to_email)
def _send_email(self, attachment):
def _send_email(self, attachment, to_email=None):
template = self.env.ref("report_async.async_report_delivery")
email_values = {}
if to_email:
email_values = {
"recipient_ids": [],
"email_to": to_email,
}
template.send_mail(
attachment.id, notif_layout="mail.mail_notification_light", force_send=False
attachment.id,
notif_layout="mail.mail_notification_light",
force_send=False,
email_values=email_values,
)

View File

@@ -3,3 +3,8 @@
* Kitti U. <kittiu@ecosoft.co.th>
* Saran Lim. <saranl@ecosoft.co.th>
* Tharathip Chaweewongphan <tharathipc@ecosoft.co.th>
* `Sunflower IT <https://sunflowerweb.nl>`__:
* Tom Blauwendraat <tom@sunflowerweb.nl>
* Kevin Kamau <kevin@sunflowerweb.nl>

View File

@@ -17,3 +17,12 @@ As normal user, you can run your reports from Report Center
- **Job Status:** show status of the latest run job. If job fail, exception error will also shown
- **Files:** show all files being produced by the job as run by the user.
- **Jobs:** show all jobs triggered by this report as run by the user. Only job queue manager have access to this button.
As additional improvement, you can now generate reports async directly from the form view itself,
and not have to go into the "Report Center" specifically to do it. This can be done by pressing
*Print*, you will get a popup asking you if you want to run it Async, and then to verify the email
address that it should be sent to. See below sample:
.. image:: ../static/description/sample.gif
:width: 800
:alt: How It Works

View File

@@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Report Async</title>
<style type="text/css">
@@ -418,6 +418,11 @@ report will be sent.</li>
<li><strong>Files:</strong> show all files being produced by the job as run by the user.</li>
<li><strong>Jobs:</strong> show all jobs triggered by this report as run by the user. Only job queue manager have access to this button.</li>
</ul>
<p>As additional improvement, you can now generate reports async directly from the form view itself,
and not have to go into the “Report Center” specifically to do it. This can be done by pressing
<em>Print</em>, you will get a popup asking you if you want to run it Async, and then to verify the email
address that it should be sent to. See below sample:</p><br/>
<img alt="How It Works" src="sample.gif" style="width: 800px;" /></br>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
@@ -444,6 +449,11 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<li>Tharathip Chaweewongphan &lt;<a class="reference external" href="mailto:tharathipc&#64;ecosoft.co.th">tharathipc&#64;ecosoft.co.th</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://sunflowerweb.nl">Sunflower IT</a>:<ul>
<li>Tom Blauwendraat &lt;<a class="reference external" href="mailto:tom&#64;sunflowerweb.nl">tom&#64;sunflowerweb.nl</a>&gt;</li>
<li>Kevin Kamau &lt;<a class="reference external" href="mailto:kevin&#64;sunflowerweb.nl">kevin&#64;sunflowerweb.nl</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -0,0 +1,128 @@
odoo.define("report_async.ActionMenus", function (require) {
"use strict";
const {patch} = require("web.utils");
const ActionMenus = require("web.ActionMenus");
const Dialog = require("web.Dialog");
const Core = require("web.core");
const Framework = require("web.framework");
const _t = Core._t;
const QWeb = Core.qweb;
function validate_email(email) {
const res = email.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
if (!res) {
return false;
}
return true;
}
// Patch _executeAction to use Dialog
patch(ActionMenus, "async _super report_async.ActionMenus", {
async _executeAction(action) {
const self = this;
const _super = this._super;
const args = arguments;
const records = this.props.activeIds;
var $content = $(QWeb.render("ReportAsyncConfiguration", {}));
if (action.async_report && records.length >= action.async_no_records) {
const asyncDialog = new Dialog(self, {
title:
_t("Async Report Configuration ") +
"(" +
action.display_name +
")",
size: "medium",
buttons: [
{
text: _t("Print"),
classes: "btn-primary",
close: true,
click: function () {
const is_report_async = this.$(
"#async_report_checker"
).prop("checked");
const save_report_attachment = this.$(
"#async-save-report-checker"
).prop("checked");
const user_email = this.$("#async-user-email").val();
if (user_email !== "" && is_report_async) {
// Try basic email validation
if (validate_email(user_email)) {
if (
"report_type" in action &&
action.report_type === "qweb-pdf"
) {
Framework.unblockUI();
// Generate report async
self.rpc({
model: "report.async",
method: "print_document_async",
args: [records, action.report_name],
kwargs: {
to_email: user_email,
data: action.data || {},
context: action.context || {},
save_attachment_to_records: save_report_attachment,
},
})
.then(() => {
const msg =
_t(
"Job started to generate report. Upon " +
"completion, mail sent to:"
) + user_email;
Dialog.alert(self, msg, {
title: _t("Report"),
});
})
.catch(() => {
const error = _t(
"Failed, error on job creation."
);
const title = _t("Report");
Dialog.alert(self, error, {
title: title,
});
});
} else {
// Default to normal approach to generate report
return _super.apply(self, args);
}
} else {
const msg = _t(
"Please check your email syntax and try again"
);
const title = _t("Email Validation Error");
Dialog.alert(self, msg, {title: title});
}
} else {
// Default to normal approach to generate report
return _super.apply(self, args);
}
},
},
{
text: _t("Discard"),
close: true,
},
],
$content: $content,
});
// Default current user mail
asyncDialog.open().opened(function () {
asyncDialog.$el
.find("#async-user-email")
.val(action.async_mail_recipient);
});
} else {
// Default to normal approach to generate report
return _super.apply(this, arguments);
}
},
});
});

View File

@@ -0,0 +1,138 @@
odoo.define("report_async.action_menus_tests", function (require) {
"use strict";
/* global QUnit*/
const ActionMenus = require("web.ActionMenus");
const Registry = require("web.Registry");
const testUtils = require("web.test_utils");
const cpHelpers = testUtils.controlPanel;
const {createComponent} = testUtils;
QUnit.module(
"report_async",
{
beforeEach() {
this.action = {
res_model: "res.users",
};
this.view = {
type: "form",
};
this.props = {
activeIds: [1, 2],
context: {},
items: {
print: [
{
type: "ir.actions.report",
async_report: true,
data: null,
async_no_records: 1,
async_mail_recipient: "admin@example.com",
report_type: "qweb-pdf",
report_name: "report_async.async_demo_report_view",
report_file: "report_async.async_demo_report_view",
name: "Async Report",
id: 1,
},
],
},
};
// Patch the registry of the action menus
this.actionMenusRegistry = ActionMenus.registry;
ActionMenus.registry = new Registry();
},
afterEach() {
ActionMenus.registry = this.actionMenusRegistry;
},
},
function () {
QUnit.test("execute print action", async function (assert) {
// No of assertion expected.
assert.expect(7);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
intercepts: {
"do-action": () => assert.step("do-action"),
},
props: this.props,
async mockRPC(route, args) {
switch (route) {
case "/web/action/load": {
const expectedContext = {
active_id: 1,
active_ids: [1, 2],
active_model: "res.users",
};
assert.deepEqual(args.context, expectedContext);
assert.step("load-action");
return {context: {}, flags: {}};
}
default:
return this._super(...arguments);
}
},
});
await testUtils.nextTick();
await cpHelpers.toggleActionMenu(actionMenus, "Print");
await cpHelpers.toggleMenuItem(actionMenus, "Async Report");
// We should have dialog created and opened
assert.containsOnce(
$,
".form",
"Error dialog should be opened and showing async options"
);
// We should have checkbox checked
assert.ok(
$("#async_report_checker").prop("checked"),
"Checkbox should be checked auto"
);
// Email should be set as default
assert.equal(
$("#async-user-email").val(),
"admin@example.com",
"Email should be set and equal to default"
);
// Try to process async report to a queue and send mail
await testUtils.dom.click($("button.btn-primary"), {
allowInvisible: true,
});
await testUtils.nextTick();
// This should fail through error/alert dialog because we haven't
// defined the report well queue job, qweb etc. For a successful
// test see possible python tests.
assert.containsNone(
$,
$(".modal-content"),
"Error Dialog should have popup"
);
assert.ok(
$(".modal-title").text(),
"Report",
'Should have title "Report"'
);
assert.ok($(".modal-content").text().search("Failed"));
// Close error dialog
await testUtils.dom.click($(".modal-footer button.btn-primary"), {
allowInvisible: true,
});
await testUtils.nextTick();
// All dialogs should be closed
assert.containsNone($, $(".modal-dialog"), "Dialogs should be closed");
// Destroy the action menus
actionMenus.destroy();
});
}
);
});

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t t-name='ReportAsyncConfiguration'>
<div class="form">
<!-- Async Checkbox -->
<div class="form-group">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
checked="checked"
id="async_report_checker"
/>
<label class="form-check-label" for="async_report_checker">
Async Report
</label>
</div>
<small id="async-report-checker-help" class="form-text text-muted">
Checker enables async report to be created on the background
via queue job and sent to a below email address.
</small>
</div>
<!-- Allow saving of attachment to record -->
<div class="form-group">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
checked="checked"
id="async-save-report-checker"
/>
<label class="form-check-label" for="async-save-report-checker">
Save attachment to records
</label>
</div>
<small id="async-save-report-checker-help" class="form-text text-muted">
Checker enables async report attachment to be created and
saved to the records. NB: Records should support attachments
</small>
</div>
<!-- Email Input -->
<div class="form-group">
<label for="async-user-email">Email Address</label>
<input
type="email"
class="form-control"
id="async-user-email"
aria-describedby="emailHelp"
placeholder="admin@example.com"
/>
<small id="async-user-email-help" class="form-text text-muted">
Email will be used to send the async report after queue job
is done on the background
</small>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template
id="report_async_assets_backend"
inherit_id="web.assets_backend"
name="Report Async Assets"
>
<xpath expr="//script[last()]" position="after">
<script
type="text/javascript"
src="/report_async/static/src/js/components/action_menus.js"
/>
</xpath>
</template>
<template
id="report_async_tests"
name="Report Async Tests"
inherit_id="web.qunit_suite_tests"
>
<xpath expr="//script[last()]" position="after">
<script
type="text/javascript"
src="/report_async/static/src/tests/report_async_tests.js"
/>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="async_act_report_xml_view" model="ir.ui.view">
<field name="name">async_report_view</field>
<field name="model">ir.actions.report</field>
<field name="inherit_id" ref="base.act_report_xml_view" />
<field name="arch" type="xml">
<xpath expr="//page[@name='advanced']/group" position="after">
<group name="async_opts" string="Async Options">
<field name="async_report" />
<field name="async_no_records" />
<field name="async_mail_recipient" />
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -85,8 +85,9 @@ odoo.define("report_qweb_encrypt.Dialog", function (require) {
})
);
reportUrls = _.mapObject(reportUrls, function (value) {
value += serializedOptionsPath;
return value;
var val = value;
val += serializedOptionsPath;
return val;
});
}
}