[IMP] purchase: Purchase order portal view

Show the purchase orders which are in state "RFQ sent" on the portal
using two separate blocks (Requests for Quotation & Purchase Orders)
as done in Sales (Quotations & Sales Orders)

closes odoo/odoo#61035

Task: 2035476
Signed-off-by: William Henrotin <Whenrow@users.noreply.github.com>
This commit is contained in:
JF Aubert
2021-04-07 13:49:46 +00:00
committed by William Henrotin
parent 51d612fa3c
commit b71a62b515
7 changed files with 646 additions and 351 deletions
+2
View File
@@ -45,6 +45,8 @@
],
'web.assets_frontend': [
'purchase/static/src/js/purchase_datetimepicker.js',
'purchase/static/src/scss/purchase_portal.scss',
'purchase/static/src/js/purchase_portal_sidebar.js',
],
'web.assets_qweb': [
'purchase/static/src/xml/**/*',
+99 -62
View File
@@ -19,12 +19,74 @@ class CustomerPortal(portal.CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
PurchaseOrder = request.env['purchase.order']
if 'rfq_count' in counters:
values['rfq_count'] = PurchaseOrder.search_count([
('state', 'in', ['sent'])
]) if PurchaseOrder.check_access_rights('read', raise_exception=False) else 0
if 'purchase_count' in counters:
values['purchase_count'] = request.env['purchase.order'].search_count([
values['purchase_count'] = PurchaseOrder.search_count([
('state', 'in', ['purchase', 'done', 'cancel'])
]) if request.env['purchase.order'].check_access_rights('read', raise_exception=False) else 0
]) if PurchaseOrder.check_access_rights('read', raise_exception=False) else 0
return values
def _render_portal(self, template, page, date_begin, date_end, sortby, filterby, domain, searchbar_filters, default_filter, url, history, page_name, key):
values = self._prepare_portal_layout_values()
PurchaseOrder = request.env['purchase.order']
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'},
'name': {'label': _('Name'), 'order': 'name asc, id asc'},
'amount_total': {'label': _('Total'), 'order': 'amount_total desc, id desc'},
}
# default sort
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
if searchbar_filters:
# default filter
if not filterby:
filterby = default_filter
domain += searchbar_filters[filterby]['domain']
# count for pager
count = PurchaseOrder.search_count(domain)
# make pager
pager = portal_pager(
url=url,
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
total=count,
page=page,
step=self._items_per_page
)
# search the purchase orders to display, according to the pager data
orders = PurchaseOrder.search(
domain,
order=order,
limit=self._items_per_page,
offset=pager['offset']
)
request.session[history] = orders.ids[:100]
values.update({
'date': date_begin,
key: orders,
'page_name': page_name,
'pager': pager,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
'default_url': url,
})
return request.render(template, values)
def _purchase_order_get_page_view_values(self, order, access_token, **kwargs):
#
def resize_to_48(b64source):
@@ -35,71 +97,46 @@ class CustomerPortal(portal.CustomerPortal):
values = {
'order': order,
'resize_to_48': resize_to_48,
'report_type': 'html',
}
return self._get_page_view_values(order, access_token, values, 'my_purchases_history', False, **kwargs)
if order.state in ('sent'):
history = 'my_rfqs_history'
else:
history = 'my_purchases_history'
return self._get_page_view_values(order, access_token, values, history, False, **kwargs)
@http.route(['/my/rfq', '/my/rfq/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_requests_for_quotation(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
return self._render_portal(
"purchase.portal_my_purchase_rfqs",
page, date_begin, date_end, sortby, filterby,
[('state', '=', 'sent')],
{},
None,
"/my/rfq",
'my_rfqs_history',
'rfq',
'rfqs'
)
@http.route(['/my/purchase', '/my/purchase/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_purchase_orders(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
values = self._prepare_portal_layout_values()
PurchaseOrder = request.env['purchase.order']
domain = []
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'},
'name': {'label': _('Name'), 'order': 'name asc, id asc'},
'amount_total': {'label': _('Total'), 'order': 'amount_total desc, id desc'},
}
# default sort by value
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
searchbar_filters = {
'all': {'label': _('All'), 'domain': [('state', 'in', ['purchase', 'done', 'cancel'])]},
'purchase': {'label': _('Purchase Order'), 'domain': [('state', '=', 'purchase')]},
'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]},
'done': {'label': _('Locked'), 'domain': [('state', '=', 'done')]},
}
# default filter by value
if not filterby:
filterby = 'all'
domain += searchbar_filters[filterby]['domain']
# count for pager
purchase_count = PurchaseOrder.search_count(domain)
# make pager
pager = portal_pager(
url="/my/purchase",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
total=purchase_count,
page=page,
step=self._items_per_page
return self._render_portal(
"purchase.portal_my_purchase_orders",
page, date_begin, date_end, sortby, filterby,
[],
{
'all': {'label': _('All'), 'domain': [('state', 'in', ['purchase', 'done', 'cancel'])]},
'purchase': {'label': _('Purchase Order'), 'domain': [('state', '=', 'purchase')]},
'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]},
'done': {'label': _('Locked'), 'domain': [('state', '=', 'done')]},
},
'all',
"/my/purchase",
'my_purchases_history',
'purchase',
'orders'
)
# search the purchase orders to display, according to the pager data
orders = PurchaseOrder.search(
domain,
order=order,
limit=self._items_per_page,
offset=pager['offset']
)
request.session['my_purchases_history'] = orders.ids[:100]
values.update({
'date': date_begin,
'orders': orders,
'page_name': 'purchase',
'pager': pager,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
'default_url': '/my/purchase',
})
return request.render("purchase.portal_my_purchase_orders", values)
@http.route(['/my/purchase/<int:order_id>'], type='http', auth="public", website=True)
def portal_my_purchase_order(self, order_id=None, access_token=None, **kw):
+4
View File
@@ -112,6 +112,10 @@
'date_planned': DateTime.today() + relativedelta(days=5)})
]"/>
</record>
<function model="purchase.order" name="write">
<value eval="[ref('purchase_order_4')]"/>
<value eval="{'state': 'sent'}"/>
</function>
<record id="purchase_order_5" model="purchase.order">
<field name="partner_id" ref="base.res_partner_2"/>
+23
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from dateutil.relativedelta import relativedelta
from functools import partial
from itertools import groupby
from pytz import timezone, UTC
from werkzeug.urls import url_encode
@@ -115,6 +116,7 @@ class PurchaseOrder(models.Model):
date_calendar_start = fields.Datetime(compute='_compute_date_calendar_start', readonly=True, store=True)
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=True)
amount_by_group = fields.Binary(string="Tax amount by group", compute='_amount_by_group', help="type: [(name, amount, base, formated amount, formated base)]")
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all')
amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all')
@@ -255,6 +257,27 @@ class PurchaseOrder(models.Model):
return self.env.ref('purchase.mt_rfq_done')
return super(PurchaseOrder, self)._track_subtype(init_values)
def _amount_by_group(self):
for order in self:
currency = order.currency_id or order.company_id.currency_id
fmt = partial(formatLang, self.with_context(lang=order.partner_id.lang).env, currency_obj=currency)
res = {}
for line in order.order_line:
taxes = line.taxes_id.compute_all(line.price_unit, quantity=line.product_qty, product=line.product_id, partner=order.partner_id)['taxes']
for tax in line.taxes_id:
group = tax.tax_group_id
res.setdefault(group, {'amount': 0.0, 'base': 0.0})
for t in taxes:
if t['id'] == tax.id or t['id'] in tax.children_tax_ids.ids:
res[group]['amount'] += t['amount']
res[group]['base'] += t['base']
res = sorted(res.items(), key=lambda l: l[0].sequence)
order.amount_by_group = [(
l[0].name, l[1]['amount'], l[1]['base'],
fmt(l[1]['amount']), fmt(l[1]['base']),
len(res),
) for l in res]
def _get_report_base_filename(self):
self.ensure_one()
return 'Purchase Order-%s' % (self.name)
@@ -0,0 +1,112 @@
odoo.define('purchase.PurchasePortalSidebar', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var PortalSidebar = require('portal.PortalSidebar');
publicWidget.registry.PurchasePortalSidebar = PortalSidebar.extend({
selector: '.o_portal_purchase_sidebar',
/**
* @constructor
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.authorizedTextTag = ['em', 'b', 'i', 'u'];
this.spyWatched = $('body[data-target=".navspy"]');
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var $spyWatcheElement = this.$el.find('[data-id="portal_sidebar"]');
this._setElementId($spyWatcheElement);
// Nav Menu ScrollSpy
this._generateMenu();
return def;
},
//--------------------------------------------------------------------------
// Private
//---------------------------------------------------------------------------
/**
* create an unique id and added as a attribute of spyWatched element
*
* @private
* @param {string} prefix
* @param {Object} $el
*
*/
_setElementId: function (prefix, $el) {
var id = _.uniqueId(prefix);
this.spyWatched.find($el).attr('id', id);
return id;
},
/**
* generate the new spy menu
*
* @private
*
*/
_generateMenu: function () {
var self = this,
lastLI = false,
lastUL = null,
$bsSidenav = this.$el.find('.bs-sidenav');
$("#quote_content [id^=quote_header_], #quote_content [id^=quote_]", this.spyWatched).attr("id", "");
_.each(this.spyWatched.find("#quote_content h2, #quote_content h3"), function (el) {
var id, text;
switch (el.tagName.toLowerCase()) {
case "h2":
id = self._setElementId('quote_header_', el);
text = self._extractText($(el));
if (!text) {
break;
}
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastUL = false;
break;
case "h3":
id = self._setElementId('quote_', el);
text = self._extractText($(el));
if (!text) {
break;
}
if (lastLI) {
if (!lastUL) {
lastUL = $("<ul class='nav flex-column'>").appendTo(lastLI);
}
$("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo(lastUL);
}
break;
}
el.setAttribute('data-anchor', true);
});
this.trigger_up('widgets_start_request', {$target: $bsSidenav});
},
/**
* extract text of menu title for sidebar
*
* @private
* @param {Object} $node
*
*/
_extractText: function ($node) {
var self = this;
var rawText = [];
_.each($node.contents(), function (el) {
var current = $(el);
if ($.trim(current.text())) {
var tagName = current.prop("tagName");
if (_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorizedTextTag, tagName.toLowerCase()))) {
rawText.push($.trim(current.text()));
}
}
});
return rawText.join(' ');
},
});
});
@@ -0,0 +1,13 @@
/* ---- My Orders page ---- */
.orders_label_text_align {
vertical-align: 15%;
}
/* ---- Order page ---- */
.purchase_tbody .o_line_note {
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
}
+393 -289
View File
@@ -3,18 +3,27 @@
<template id="portal_my_home_menu_purchase" name="Portal layout : purchase menu entries" inherit_id="portal.portal_breadcrumbs" priority="25">
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li t-if="page_name == 'purchase' or purchase_order" t-attf-class="breadcrumb-item #{'active ' if not purchase_order else ''}">
<a t-if="purchase_order" t-attf-href="/my/purchase?{{ keep_query() }}">Purchase Orders</a>
<li t-if="page_name == 'rfq' or order and order.state == 'sent'" t-attf-class="breadcrumb-item #{'active ' if not order else ''}">
<a t-if="order" t-attf-href="/my/rfq?{{ keep_query() }}">Requests for Quotation</a>
<t t-else="">Requests for Quotation</t>
</li>
<li t-if="page_name == 'purchase' or order and order.state != 'sent'" t-attf-class="breadcrumb-item #{'active ' if not order else ''}">
<a t-if="order" t-attf-href="/my/purchase?{{ keep_query() }}">Purchase Orders</a>
<t t-else="">Purchase Orders</t>
</li>
<li t-if="purchase_order" class="breadcrumb-item active">
<t t-esc="purchase_order.name"/>
<li t-if="order" class="breadcrumb-item active">
<t t-esc="order.name"/>
</li>
</xpath>
</template>
<template id="portal_my_home_purchase" name="Show Purchase Orders" customize_show="True" inherit_id="portal.portal_my_home" priority="25">
<template id="portal_my_home_purchase" name="Show Requests for Quotation / Purchase Orders" customize_show="True" inherit_id="portal.portal_my_home" priority="25">
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-call="portal.portal_docs_entry">
<t t-set="title">Requests for Quotation</t>
<t t-set="url" t-value="'/my/rfq'"/>
<t t-set="placeholder_count" t-value="'rfq_count'"/>
</t>
<t t-call="portal.portal_docs_entry">
<t t-set="title">Purchase Orders</t>
<t t-set="url" t-value="'/my/purchase'"/>
@@ -23,302 +32,397 @@
</xpath>
</template>
<template id="portal_my_purchase_orders" name="Portal: My Purchase Orders">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="portal.portal_searchbar"/>
<t t-if="orders" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>Purchase Orders #</th>
<th class="text-right">Confirmation Date</th>
<th></th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
<t t-foreach="orders" t-as="order">
<tr>
<td>
<a t-att-href="order.get_portal_url()">
<t t-esc="order.name"/>
</a>
</td>
<td class="text-right">
<span t-field="order.date_approve"/>
</td>
<td>
<t t-if="order.invoice_status == 'to invoice'">
<span class="badge badge-info"><i class="fa fa-fw fa-file-text"/> Waiting for Bill</span>
</t>
<t t-if="order.state == 'cancel'">
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
</t>
</td>
<td class="text-right">
<span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
<template id="portal_my_purchase_rfqs" name="My Requests For Quotation">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="portal.portal_searchbar">
<t t-set="title" >Requests For Quotation</t>
</t>
<t t-if="not rfqs">
<p>There are currently no requests for quotation for your account.</p>
</t>
<t t-if="rfqs" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>
<span class='d-none d-md-inline'>Request for Quotation #</span>
<span class='d-block d-md-none'>Ref.</span>
</th>
<th class="text-right">Order Deadline</th>
<th class="text-right">Total</th>
</tr>
</thead>
<t t-foreach="rfqs" t-as="rfq">
<tr>
<td><a t-att-href="rfq.get_portal_url()"><t t-esc="rfq.name"/></a></td>
<td class="text-right">
<span t-field="rfq.date_order" t-options="{'widget': 'date'}"/>&amp;nbsp;
<span class='d-none d-md-inline' t-field="rfq.date_order" t-options="{'time_only': True}"/>
</td>
<td class="text-right">
<span t-field="rfq.amount_total"/>
</td>
</tr>
</t>
</t>
</tbody>
</t>
</t>
</template>
<template id="portal_my_purchase_order" name="Portal: My Purchase Order">
<t t-call="portal.portal_layout">
<t t-set="purchase_order" t-value="order"/>
<t t-set="o_portal_fullwidth_alert" groups="purchase.group_purchase_manager">
<t t-call="portal.portal_back_in_edit_mode">
<t t-set="backend_url" t-value="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (purchase_order._name, purchase_order.id, purchase_order.env.ref('purchase.purchase_rfq').id)"/>
</t>
</t>
<div id="optional_placeholder"></div>
<div class="container">
<div class="row mt16 o_portal_purchase_sidebar">
<!-- Sidebar -->
<t t-call="portal.portal_record_sidebar">
<t t-set="classes" t-value="'col-lg-auto d-print-none'"/>
<t t-set="title">
<h2 class="mb-0">
<b t-field="purchase_order.amount_total" data-id="total_amount"/>
</h2>
</t>
<t t-set="entries">
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
<li class="list-group-item flex-grow-1">
<div class="o_download_pdf btn-toolbar flex-sm-nowrap">
<div class="btn-group flex-grow-1 mr-1 mb-1">
<a class="btn btn-secondary btn-block o_download_btn" t-att-href="purchase_order.get_portal_url(report_type='pdf', download=True)" title="Download"><i class="fa fa-download"/> Download</a>
</div>
<div class="btn-group flex-grow-1 mb-1">
<a class="btn btn-secondary btn-block o_print_btn o_portal_invoice_print" t-att-href="purchase_order.get_portal_url(report_type='pdf')" id="print_invoice_report" title="Print" target="_blank"><i class="fa fa-print"/> Print</a>
</div>
</div>
</li>
<template id="portal_my_purchase_orders" name="My Purchase Orders">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<li t-if="purchase_order.user_id" class="list-group-item flex-grow-1">
<div class="small mb-1">
<strong class="text-muted">Purchase Representative</strong>
</div>
<div class="row flex-nowrap">
<div class="col flex-grow-0 pr-2">
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="purchase_order.user_id.image_1024" t-att-src="image_data_uri(purchase_order.user_id.image_1024)" alt="Contact"/>
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="not purchase_order.user_id.image_1024" src="/web/static/src/img/placeholder.png" alt="Contact"/>
</div>
<div class="col pl-0" style="min-width: 150px">
<span t-field="purchase_order.user_id" t-options='{"widget": "contact", "fields": ["name", "phone"], "no_marker": True}'/>
<a href="#discussion" class="small"><i class="fa fa-comment"></i> Send message</a>
</div>
</div>
</li>
</ul>
</t>
<t t-call="portal.portal_searchbar">
<t t-set="title">Purchase Orders</t>
</t>
<div class=" col-lg col-12 justify-content-end w-100">
<div class= "card pb-5">
<div class="card-header bg-white pb-1">
<div class="row">
<div class="col-lg-12">
<h2 class="font-weight-normal">
<t t-if="order.state in ['draft', 'sent']">Request for Quotation</t>
<t t-else="1">
Purchase Order
</t>
<span class="font-italic" t-esc="order.name"/>
</h2>
</div>
</div>
</div>
<div class="card-body">
<div class="mb-4">
<strong class="d-block mb-1">From:</strong>
<address t-field="order.company_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
<strong>Confirmation Date:</strong> <span t-field="order.date_approve" t-options='{"widget": "date"}'/><br/>
<div t-att-class="'d-inline' if order.date_planned else 'd-none'">
<strong>Receipt Date:</strong><span class="ml-1" t-field="order.date_planned" t-options='{"widget": "date"}'/>
</div>
</div>
<h3 class="font-weight-normal">Pricing</h3>
<table class="table table-sm">
<thead class="bg-100">
<tr>
<th>Products</th>
<th class="text-right d-none d-sm-table-cell">Unit Price</th>
<th class="text-right">Quantity</th>
<th class="text-right">Subtotal</th>
</tr>
</thead>
<tbody>
<t t-foreach="order.order_line" t-as="ol">
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if ol.display_type == 'line_section' else 'font-italic o_line_note' if ol.display_type == 'line_note' else ''">
<t t-if="not ol.display_type">
<td>
<img t-att-src="image_data_uri(resize_to_48(ol.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/>
<span t-esc="ol.name"/>
</td>
<td class="text-right d-none d-sm-table-cell">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
<td class="text-right">
<span t-esc="ol.product_qty"/>
</td>
<td class="text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</t>
<t t-if="ol.display_type == 'line_section'">
<td colspan="99">
<span t-field="ol.name"/>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<t t-if="ol.display_type == 'line_note'">
<td colspan="99">
<span t-field="ol.name"/>
</td>
</t>
</tr>
</t>
</tbody>
</table>
<div class="row">
<div class="col-sm-7 col-md-5 ml-auto">
<table class="table table-sm">
<tbody>
<tr>
<td>
<strong>Untaxed Amount:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
<tr>
<td>
<strong>Taxes:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
<tr>
<td>
<strong>Total:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="purchase_order_communication" class="mt-4">
<h2>History</h2>
<t t-call="portal.message_thread">
<t t-set="object" t-value="purchase_order"/>
<t t-if="not orders">
<p>There are currently no purchase orders for your account.</p>
</t>
<t t-if="orders" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>
<span class='d-none d-md-inline'>Purchase Order #</span>
<span class='d-block d-md-none'>Ref.</span>
</th>
<th class="text-right">
<span class='d-none d-md-inline'>Confirmation Date</span>
<span class='d-block d-md-none'>Confirmation</span>
</th>
<th class="text-center"/>
<th class="text-right">Total</th>
</tr>
</thead>
<t t-foreach="orders" t-as="order">
<tr>
<td><a t-att-href="order.get_portal_url()"><t t-esc="order.name"/></a></td>
<td class="text-right">
<span t-field="order.date_approve" t-options="{'widget': 'date'}"/>&amp;nbsp;
<span class='d-none d-md-inline' t-field="order.date_approve" t-options="{'time_only': True}"/>
</td>
<td class="text-center">
<span t-if="order.invoice_status == 'to_invoice'" class="badge badge-pill badge-info">
<i class="fa fa-fw fa-file-text" role="img" aria-label="Waiting for Bill" title="Waiting for Bill"></i><span class="d-none d-md-inline"> Waiting for Bill</span>
</span>
<span t-if="order.state == 'cancel'" class="badge badge-pill badge-secondary">
<i class="fa fa-fw fa-remove" role="img" aria-label="Cancelled" title="Cancelled"></i><span class="d-none d-md-inline"> Cancelled</span>
</span>
<span t-if="order.state == 'done'" class="badge badge-pill badge-success">
<i class="fa fa-fw fa-check" role="img" aria-label="Done" title="Done"></i><span class="d-none d-md-inline"> Done</span>
</span>
</td>
<td class="text-right"><span t-field="order.amount_total"/></td>
</tr>
</t>
</div>
</t>
</t>
</template>
<template id="portal_my_purchase_order" name="Purchase Order Portal Template" inherit_id="portal.portal_sidebar" primary="True">
<xpath expr="//div[hasclass('o_portal_sidebar')]" position="inside">
<t t-set="o_portal_fullwidth_alert" groups="purchase.group_purchase_manager">
<t t-call="portal.portal_back_in_edit_mode">
<t t-set="backend_url" t-value="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % (order._name, order.id, order.env.ref('purchase.purchase_rfq').id)"/>
</t>
</t>
<div class="row mt16 o_portal_purchase_sidebar">
<!-- Sidebar -->
<t t-call="portal.portal_record_sidebar">
<t t-set="classes" t-value="'col-lg-auto d-print-none'"/>
<t t-set="title">
<h2 class="mb-0"><b t-field="order.amount_total" data-id="total_amount"/> </h2>
</t>
<t t-set="entries">
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
<li class="list-group-item flex-grow-1">
<div class="o_download_pdf btn-toolbar flex-sm-nowrap">
<div class="btn-group flex-grow-1 mr-1 mb-1">
<a class="btn btn-secondary btn-block o_download_btn" t-att-href="order.get_portal_url(report_type='pdf', download=True)" title="Download"><i class="fa fa-download"/> Download</a>
</div>
<div class="btn-group flex-grow-1 mb-1">
<a class="btn btn-secondary btn-block o_print_btn o_portal_invoice_print" t-att-href="order.get_portal_url(report_type='pdf')" id="print_invoice_report" title="Print" target="_blank"><i class="fa fa-print"/> Print</a>
</div>
</div>
</li>
<li class="navspy list-group-item pl-0 flex-grow-1" t-ignore="true" role="complementary">
<ul class="nav flex-column bs-sidenav"></ul>
</li>
<li t-if="order.user_id" class="list-group-item flex-grow-1">
<div class="small mb-1"><strong class="text-muted">Purchase Representative</strong></div>
<div class="row flex-nowrap">
<div class="col flex-grow-0 pr-2">
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="order.user_id.image_1024" t-att-src="image_data_uri(order.user_id.image_1024)" alt="Contact"/>
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="not order.user_id.image_1024" src="/web/static/src/img/placeholder.png" alt="Contact"/>
</div>
<div class="col pl-0" style="min-width: 150px">
<span t-field="order.user_id" t-options='{"widget": "contact", "fields": ["name", "phone"], "no_marker": True}'/>
<a href="#discussion" class="small"><i class="fa fa-comment"></i> Send message</a>
</div>
</div>
</li>
</ul>
</t>
</t>
<!-- Page content -->
<div id="quote_content" class="col-12 col-lg justify-content-end">
<!-- status messages -->
<div t-if="order.state == 'cancel'" class="alert alert-danger alert-dismissable d-print-none" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="close">&amp;times;</button>
<strong>This purchase has been canceled.</strong>
</div>
<!-- main content -->
<div t-attf-class="card #{'pb-5' if report_type == 'html' else ''}" id="portal_purchase_content">
<div t-call="purchase.purchase_order_portal_content"/>
</div>
<!-- chatter -->
<div id="purchase_order_communication" class="mt-4">
<h2>History</h2>
<t t-call="portal.message_thread">
<t t-set="object" t-value="order"/>
</t>
</div>
</div><!-- // #quote_content -->
</div>
</div>
</xpath>
</template>
<template id="purchase_order_portal_content" name="Purchase Order Portal Content">
<!-- Intro -->
<div id="introduction" t-attf-class="pb-2 pt-3 #{'card-header bg-white' if report_type == 'html' else ''}">
<h2 class="my-0">
<t t-if="order.state in ['draft', 'sent']">Request for Quotation</t>
<t t-else="1">
Purchase Order
</t>
<em t-esc="order.name"/>
</h2>
</div>
<div class="oe_structure mb32"/>
</t>
<div t-attf-class="#{'card-body' if report_type == 'html' else ''}">
<!-- Informations -->
<div id="informations">
<div class="row" id="po_date">
<div class="mb-3 col-6">
<t t-if="order.state in ['draft', 'sent']">
<strong>Request For Quotation Date:</strong>
</t>
<t t-if="order.state in ['purchase', 'done', 'cancel']">
<strong>Order Date:</strong>
</t>
<span t-field="order.date_order" t-options='{"widget": "date"}'/>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<strong class="d-block mb-1">From:</strong>
<address t-field="order.company_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
<strong>Confirmation Date:</strong> <span t-field="order.date_approve" t-options='{"widget": "date"}'/><br/>
<div t-att-class="'d-inline' if order.date_planned else 'd-none'">
<strong>Receipt Date:</strong><span class="ml-1" t-field="order.date_planned" t-options='{"widget": "date"}'/>
</div>
</div>
</div>
<t t-set="invoices" t-value="[i for i in order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
<div t-if="invoices" class="row">
<div class="col">
<strong class="d-block mb-1">Invoices</strong>
<ul class="list-group mb-4">
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="i.get_portal_url(report_type='pdf', download=True)"/>
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div>
<a t-att-href="report_url">
<span t-esc="i.name"/>
</a>
<div class="small d-lg-inline-block">Date: <span class="text-muted" t-field="i.invoice_date"/></div>
</div>
<span t-if="i.payment_state in ('paid', 'in_payment')" class="small badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> <b>Paid</b></span>
<span t-else="" class="small badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> <b>Waiting Payment</b></span>
</div>
</t>
</ul>
</div>
</div>
</div>
<section id="details" style="page-break-inside: auto;" class="mt32">
<h3 id="details">Pricing</h3>
<table t-att-data-order-id="order.id" t-att-data-token="order.access_token" class="table table-sm" id="purchase_order_table">
<thead class="bg-100">
<tr>
<th class="text-left">Products</th>
<th class="text-right">Quantity</th>
<th t-if="update_dates" class="text-right">Scheduled Date</th>
<th t-if="update_dates" class="text-right"><strong>Update Here</strong></th>
<th t-if="not update_dates" t-attf-class="text-right {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">Unit Price</th>
<th t-if="not update_dates" t-attf-class="text-right {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">
<span>Taxes</span>
</th>
<th class="text-right" >
<span groups="account.group_show_line_subtotals_tax_excluded">Amount</span>
<span groups="account.group_show_line_subtotals_tax_included">Total Price</span>
</th>
</tr>
</thead>
<tbody class="purchase_tbody">
<t t-set="current_subtotal" t-value="0"/>
<t t-foreach="order.order_line" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''">
<t t-if="not line.display_type">
<td id="product_name">
<img t-att-src="image_data_uri(resize_to_48(line.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/>
<span t-field="line.name"/>
</td>
<td class="text-right">
<div id="quote_qty">
<span t-field="line.product_qty"/>
<span t-field="line.product_uom" groups="uom.group_uom"/>
</div>
</td>
<td t-if="update_dates" class="text-right"><span t-esc="line.date_planned.date()"/></td>
<td t-if="update_dates" class="text-right">
<form t-attf-action="/my/purchase/#{order.id}/update?access_token=#{order.access_token}" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="container">
<div class="form-group">
<div class="input-group date">
<input type="text" class="form-control datetimepicker-input o-purchase-datetimepicker" t-attf-id="datetimepicker_#{line.id}" t-att-name="line.id"
data-toggle="datetimepicker" data-date-format="YYYY-MM-DD" t-attf-data-target="#datetimepicker_#{line.id}"/>
</div>
</div>
</div>
</form>
</td>
<td t-if="not update_dates" t-attf-class="text-right {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">
<div
t-field="line.price_unit"
class="text-right"
/>
</td>
<td t-if="not update_dates" t-attf-class="text-right {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.taxes_id))"/>
</td>
<td class="text-right">
<span class="oe_order_line_price_subtotal" t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
<span class="oe_order_line_price_total" t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
<td colspan="99">
<span t-field="line.name"/>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<t t-if="line.display_type == 'line_note'">
<td colspan="99">
<span t-field="line.name"/>
</td>
</t>
</tr>
<t t-if="current_section and (line_last or order.order_line[line_index+1].display_type == 'line_section')">
<tr class="is-subtotal text-right">
<td colspan="99">
<strong class="mr16">Subtotal</strong>
<span
t-esc="current_subtotal"
t-options='{"widget": "monetary", "display_currency": order.currency_id}'
/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<div id="total" class="row" name="total" style="page-break-inside: avoid;">
<div t-attf-class="#{'col-4' if report_type != 'html' else 'col-sm-7 col-md-5'} ml-auto">
<t t-call="purchase.purchase_order_portal_content_totals_table"/>
</div>
</div>
</section>
<section id="terms" class="mt-5" t-if="order.notes">
<h3 class="">Terms &amp; Conditions</h3>
<hr class="mt-0 mb-1"/>
<em t-field="order.notes"/>
</section>
<section class="mt-5" t-if="order.payment_term_id">
<h3 class="">Payment terms</h3>
<hr class="mt-0 mb-1"/>
<span t-field="order.payment_term_id"/>
</section>
</div>
</template>
<template id="purchase_order_portal_content_totals_table">
<table class="table table-sm">
<tr class="border-black">
<td><strong>Subtotal</strong></td>
<td class="text-right">
<span
data-id="total_untaxed"
t-field="order.amount_untaxed"
t-options='{"widget": "monetary","display_currency": order.currency_id}'
/>
</td>
</tr>
<t t-foreach="order.amount_by_group" t-as="amount_by_group">
<tr>
<t t-if="amount_by_group[3] == 1 and order.amount_untaxed == amount_by_group[2]">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/></span>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</t>
<t t-else ="">
<td>
<span t-esc="amount_by_group[0]"/>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</t>
</tr>
</t>
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span data-id="total_amount" t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
</table>
</template>
<template id="portal_my_purchase_order_update_date" name="Portal: My Purchase Order Update Dates" inherit_id="purchase.portal_my_purchase_order" primary="True">
<xpath expr="//div[hasclass('card-body')]" position="replace">
<div class="card-body">
<div class="mb-4">
<strong>Confirmation Date:</strong> <span t-field="order.date_approve" t-options='{"widget": "date"}'/><br/>
<div t-att-class="'d-inline' if order.date_planned else 'd-none'">
<strong>Receipt Date:</strong><span class="ml-1" t-field="order.date_planned" t-options='{"widget": "date"}'/>
</div>
</div>
<h3 class="font-weight-normal">Pricing</h3>
<table class="table table-sm">
<thead class="bg-100">
<tr>
<th>Products</th>
<th class="text-right d-none d-sm-table-cell">Unit Price</th>
<th class="text-right">Quantity</th>
<th class="text-right">Scheduled Date</th>
<th class="text-right" style="color:#3aadaa"><strong>Update Dates Here</strong></th>
<th class="text-right">Subtotal</th>
</tr>
</thead>
<tbody>
<t t-foreach="order.order_line" t-as="ol">
<tr>
<td>
<img t-att-src="image_data_uri(resize_to_48(ol.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/>
<span t-esc="ol.name"/>
</td>
<td class="text-right d-none d-sm-table-cell">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
<td class="text-right">
<span t-esc="ol.product_qty"/>
</td>
<td class="text-right">
<span t-esc="ol.date_planned.date()"/>
</td>
<td class="text-right">
<form t-attf-action="/my/purchase/#{order.id}/update?access_token=#{order.access_token}" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="container">
<div class="form-group">
<div class="input-group date">
<input type="text" class="form-control datetimepicker-input o-purchase-datetimepicker" t-attf-id="datetimepicker_#{ol.id}" t-att-name="ol.id"
data-toggle="datetimepicker" data-date-format="YYYY-MM-DD" t-attf-data-target="#datetimepicker_#{ol.id}"/>
</div>
</div>
</div>
</form>
</td>
<td class="text-right">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
</t>
</tbody>
</table>
<div class="row">
<div class="col-sm-7 col-md-5 ml-auto">
<table class="table table-sm">
<tbody>
<tr>
<td>
<strong>Untaxed Amount:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
<tr>
<td>
<strong>Taxes:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
<tr>
<td>
<strong>Total:</strong>
</td>
<td class="text-right">
<span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<xpath expr="////div[@id='portal_purchase_content']" position="replace">
<div t-attf-class="card #{'pb-5' if report_type == 'html' else ''}" id="portal_purchase_content">
<t t-set="update_dates" t-value="True"/>
<div t-call="purchase.purchase_order_portal_content"/>
</div>
</xpath>
</template>