[IMP] hr_expense: expense report

This pr aim to improve the pdf of the expense report. we have changed the design
 of the header of the report, the table and also added an annex with the
 different attachments.

 For the attachments, when it's a pdf it's simply merge to the actual pdf but
 when it's an image we use a new template to display the name of the expense
 link to that image.

closes odoo/odoo#133611

Task: 3443413
Signed-off-by: Olivier Colson (oco) <oco@odoo.com>
This commit is contained in:
Maximilien (malb)
2023-09-14 15:50:16 +00:00
parent d415d252ad
commit 49d6bdb675
5 changed files with 208 additions and 69 deletions
+3
View File
@@ -66,6 +66,9 @@ This module also uses analytic accounting and is compatible with the invoice on
'web.assets_tests': [
'hr_expense/static/tests/tours/expense_upload_tours.js',
],
'web.report_assets_common': [
'hr_expense/static/src/scss/hr_expense.scss',
],
},
'license': 'LGPL-3',
}
+1
View File
@@ -12,3 +12,4 @@ from . import res_config_settings
from . import account_journal_dashboard
from . import res_company
from . import analytic
from . import ir_actions_report
@@ -0,0 +1,47 @@
import io
from odoo import models
from odoo.tools import pdf
from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
# OVERRIDE
res = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids)
if not res_ids:
return res
report = self._get_report(report_ref)
if report.report_name == 'hr_expense.report_expense_sheet':
expense_sheets = self.env['hr.expense.sheet'].browse(res_ids)
for expense_sheet in expense_sheets:
# Will contains the expense report
stream_list = []
stream = res[expense_sheet.id]['stream']
stream_list.append(stream)
attachments = self.env['ir.attachment'].search([('res_id', 'in', expense_sheet.expense_line_ids.ids), ('res_model', '=', 'hr.expense')])
expense_report = OdooPdfFileReader(stream, strict=False)
output_pdf = OdooPdfFileWriter()
output_pdf.appendPagesFromReader(expense_report)
for attachment in attachments:
if attachment.mimetype == 'application/pdf':
attachment_stream = pdf.to_pdf_stream(attachment)
else:
# In case the attachment is not a pdf we will create a new PDF from the template "report_expense_sheet_img"
# And then append to the stream. By doing so, the attachment is put on a new page with the name of the expense
# associated to the attachment
data['attachment'] = attachment
attachment_prep_stream = self._render_qweb_pdf_prepare_streams('hr_expense.report_expense_sheet_img', data, res_ids=res_ids)
attachment_stream = attachment_prep_stream[expense_sheet.id]['stream']
attachment_reader = OdooPdfFileReader(attachment_stream, strict=False)
output_pdf.appendPagesFromReader(attachment_reader)
stream_list.append(attachment_stream)
new_pdf_stream = io.BytesIO()
output_pdf.write(new_pdf_stream)
res[expense_sheet.id]['stream'] = new_pdf_stream
for stream in stream_list:
stream.close()
return res
+77 -69
View File
@@ -4,101 +4,102 @@
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<div class="page o_content_pdf">
<h2>Expenses Report</h2>
<div class="row mt32 mb32">
<div class="col-2">
<strong>Employee:</strong>
<p t-field="o.employee_id.name"/>
<h3><t t-out="o.name"/></h3>
<div class="row o_header">
<div class="col-6">
<div class="row">
<div class="col-3 fw-bold"><span>Employee:</span> </div>
<div class="col-9 text-muted"><span t-field="o.employee_id.name"/></div>
</div>
<div class="row" t-if="o.accounting_date">
<div class="col-3 fw-bold"><span>Date:</span></div>
<div class="col-9 text-muted"><span t-field="o.accounting_date"/></div>
</div>
</div>
<div class="col-2">
<strong>Date:</strong>
<p t-field="o.accounting_date"/>
</div>
<div class="col-3">
<strong>Description:</strong>
<p t-field="o.name"/>
</div>
<div class="col-2">
<strong>Validated By:</strong>
<p t-field="o.user_id"/>
</div>
<div class="col-3">
<strong>Payment By:</strong>
<p t-field="o.payment_mode"/>
<div class="col-6">
<div class="row" t-if="o.user_id.name">
<div class="col-3 fw-bold"><span>Manager:</span></div>
<div class="col-9 text-muted"><span t-field="o.user_id"/></div>
</div>
<div class="row">
<div class="col-3 fw-bold"><span>Paid by:</span></div>
<div class="col-9 text-muted"><span t-field="o.payment_mode"/></div>
</div>
</div>
</div>
<table class="table table-sm">
<table class="o_table">
<thead>
<tr>
<th>Date</th>
<th>Name</th>
<th class="text-center">Ref.</th>
<th>Unit Price</th>
<th>Taxes</th>
<th class="text-center">Qty</th>
<th class="text-end">Price</th>
<th t-if="o.is_multiple_currency" class="text-end">Price in Company Currency</th>
<th class="text-start">Date</th>
<th class="text-start">Name</th>
<th class="text-end">Unit Price</th>
<th class="text-end">Quantity</th>
<th class="text-end">Taxes</th>
<t t-set="foreign_currencies" t-value="o.expense_line_ids.currency_id - o.company_currency_id"/>
<th t-if="foreign_currencies" class="text-end">Subtotal in currency</th>
<th class="text-end">Subtotal</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.expense_line_ids" t-as="line">
<td><span t-field="line.date"/></td>
<td>
<span t-field="line.name"/>
<span t-field="line.description"/><br/>
<td class="text-start"><span t-field="line.date"/></td>
<td t-att-class="'text-start' + (' o_overflow' if len(line.name) > 30 else '')">
<span t-field="line.name"/><br/>
<span>
<div t-foreach="line.analytic_distribution" t-as="distribution">
<t t-esc="line.env['account.analytic.account'].browse(int(distribution))[0].name"/>: <t t-esc="line.analytic_distribution.get(distribution)"/>
<t t-out="line.env['account.analytic.account'].browse(int(distribution))[0].name"/>: <t t-out="line.analytic_distribution.get(distribution)"/>
</div>
</span>
</td>
<td style="text-center">
<span t-field="line.reference"/>
</td>
<td>
<span t-field="line.unit_amount"/>
</td>
<td>
<td class="text-end"><span t-field="line.unit_amount"/></td>
<td class="text-end"><span t-field="line.quantity"/></td>
<td class="text-end">
<t t-foreach="line.tax_ids" t-as="tax">
<t t-if="tax.description">
<span t-field="tax.description"/>
</t>
<t t-if="not tax.description">
<span t-field="tax.name"/>
</t>
<t t-if="tax.description"><span t-field="tax.description"/></t>
<t t-if="not tax.description"><span t-field="tax.name"/></t>
</t>
</td>
<td class="text-center">
<span t-field="line.quantity"/>
</td>
<td class="text-end">
<span t-field="line.total_amount"
t-options='{"widget": "monetary", "display_currency": line.currency_id}'/>
</td>
<td t-if="o.is_multiple_currency" class="text-end">
<span t-field="line.total_amount_company"/>
</td>
<td t-if="foreign_currencies" class="text-end"><span t-field="line.total_amount" t-options='{"widget": "monetary", "display_currency": line.currency_id}'/></td>
<td class="text-end"><span t-field="line.total_amount_company" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
</tr>
</tbody>
</table>
<div class="row justify-content-end">
<div class="row justify-content-end o_total">
<div class="col-4">
<table class="table table-sm">
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-end">
<span t-field="o.total_amount"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
<table class="o_subtable">
<tbody>
<tr>
<td>Untaxed Amount</td>
<td class="text-end"><span t-field="o.untaxed_amount" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td>
</tr>
<tr>
<td>Taxes</td>
<td class="text-end"><span t-field="o.total_amount_taxes" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td>
</tr>
<tr class="fw-bold">
<td>Total</td>
<td class="text-end"><span t-field="o.total_amount" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p>
</div>
</t>
</t>
</t>
</template>
<template id="report_expense_sheet_img">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.basic_layout">
<div t-if="attachment and attachment.mimetype != 'application/pdf'">
<h3> <t t-out="attachment.res_name"/> </h3>
<img t-att-src="attachment.image_src" class="o_attachment_pdf"/>
</div>
</t>
</t>
@@ -116,4 +117,11 @@
<field name="binding_type">report</field>
</record>
<record id="action_report_expense_sheet_img" model="ir.actions.report">
<field name="name">Expense Report Image</field>
<field name="model">hr.expense.sheet</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">hr_expense.report_expense_sheet_img</field>
<field name="report_file">hr_expense.report_expense_sheet_img</field>
</record>
</odoo>
@@ -47,3 +47,83 @@
.o_expense_categories td[name="description"] p:last-child {
margin-bottom: 0;
}
/**************
PDF EXPORT
***************/
.o_content_pdf {
.o_header {
vertical-align: middle;
.col-6 {
.row:first-child{
padding-top: 15px;
}
.row:last-child{
padding-bottom: 15px;
}
}
}
.o_table {
width: 100%;
thead, tbody, tfoot, tr, td, th {
border-style: none;
}
/*****************
Header of the main table
*****************/
thead > tr > th {
padding: 10px 5px 10px 5px;
font-weight: bold;
font-size: 1rem;
border-bottom: 1px solid black;
white-space: nowrap;
}
tbody > tr {
white-space: nowrap;
td:not(:last-child) {
padding: 5px;
}
td:last-child {
padding: 5px 5px 20px 5px;
}
}
}
.o_total {
page-break-inside: avoid;
position:relative;
.o_subtable {
width: 100%;
page-break-inside: avoid;
position:relative;
tbody, tr, td{
border-style: none;
padding: 10px 0 10px 0;
}
tr:first-child {
border-top: 1px solid black;
}
tr:last-child {
border-top:1px solid black;
}
}
}
}
.o_end_page {
page-break-after: always;
}
.o_attachment_pdf {
max-height: 700px;
max-width: 700px;
}
.o_overflow {
white-space: normal !important;
overflow-wrap: break-word;
}