From b71a62b5159f7ffbf9257b6aaf8a93d00e745f6b Mon Sep 17 00:00:00 2001 From: JF Aubert Date: Thu, 7 Jan 2021 12:56:17 +0000 Subject: [PATCH] [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 --- addons/purchase/__manifest__.py | 2 + addons/purchase/controllers/portal.py | 161 +++-- addons/purchase/data/purchase_demo.xml | 4 + addons/purchase/models/purchase.py | 23 + .../static/src/js/purchase_portal_sidebar.js | 112 +++ .../static/src/scss/purchase_portal.scss | 13 + addons/purchase/views/portal_templates.xml | 682 ++++++++++-------- 7 files changed, 646 insertions(+), 351 deletions(-) create mode 100644 addons/purchase/static/src/js/purchase_portal_sidebar.js create mode 100644 addons/purchase/static/src/scss/purchase_portal.scss diff --git a/addons/purchase/__manifest__.py b/addons/purchase/__manifest__.py index 2375def6c3e..b9fb5018127 100644 --- a/addons/purchase/__manifest__.py +++ b/addons/purchase/__manifest__.py @@ -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/**/*', diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py index 60de5098fd2..c88648155e3 100644 --- a/addons/purchase/controllers/portal.py +++ b/addons/purchase/controllers/portal.py @@ -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/'], 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/'], 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/'], type='http', auth="public", website=True) def portal_my_purchase_order(self, order_id=None, access_token=None, **kw): diff --git a/addons/purchase/data/purchase_demo.xml b/addons/purchase/data/purchase_demo.xml index fa17b083632..c644e81e6d7 100644 --- a/addons/purchase/data/purchase_demo.xml +++ b/addons/purchase/data/purchase_demo.xml @@ -112,6 +112,10 @@ 'date_planned': DateTime.today() + relativedelta(days=5)}) ]"/> + + + + diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index ed078e31342..b1eae6ecba8 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -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) diff --git a/addons/purchase/static/src/js/purchase_portal_sidebar.js b/addons/purchase/static/src/js/purchase_portal_sidebar.js new file mode 100644 index 00000000000..20a9b6b57d9 --- /dev/null +++ b/addons/purchase/static/src/js/purchase_portal_sidebar.js @@ -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 = $("