Prior to this commit:
* The portal timesheet section was only visible in invoices when
invoicing policy was set to delivery.
* The portal timesheet section was not visible on orders.
After this commit:
* The portal timesheet section will be displayed for both delivery and order invoicing
policies.
* The portal timesheet section will be available for orders.
task-2388500
closes odoo/odoo#62516
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
335 lines
14 KiB
Python
335 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import binascii
|
|
|
|
from odoo import fields, http, _
|
|
from odoo.exceptions import AccessError, MissingError
|
|
from odoo.http import request
|
|
from odoo.addons.portal.controllers import portal
|
|
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
|
from odoo.addons.portal.controllers.mail import _message_post_helper
|
|
from odoo.addons.portal.controllers.portal import pager as portal_pager, get_records_pager
|
|
from odoo.osv import expression
|
|
|
|
|
|
class CustomerPortal(portal.CustomerPortal):
|
|
|
|
def _prepare_home_portal_values(self, counters):
|
|
values = super()._prepare_home_portal_values(counters)
|
|
partner = request.env.user.partner_id
|
|
|
|
SaleOrder = request.env['sale.order']
|
|
if 'quotation_count' in counters:
|
|
values['quotation_count'] = SaleOrder.search_count([
|
|
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
|
('state', 'in', ['sent', 'cancel'])
|
|
]) if SaleOrder.check_access_rights('read', raise_exception=False) else 0
|
|
if 'order_count' in counters:
|
|
values['order_count'] = SaleOrder.search_count([
|
|
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
|
('state', 'in', ['sale', 'done'])
|
|
]) if SaleOrder.check_access_rights('read', raise_exception=False) else 0
|
|
|
|
return values
|
|
|
|
def _order_get_page_view_values(self, order, access_token, **kwargs):
|
|
values = {
|
|
'sale_order': order,
|
|
'token': access_token,
|
|
'return_url': '/shop/payment/validate',
|
|
'bootstrap_formatting': True,
|
|
'partner_id': order.partner_id.id,
|
|
'report_type': 'html',
|
|
'action': order._get_portal_return_action(),
|
|
}
|
|
if order.company_id:
|
|
values['res_company'] = order.company_id
|
|
|
|
if order.has_to_be_paid():
|
|
domain = expression.AND([
|
|
['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', order.company_id.id)],
|
|
['|', ('country_ids', '=', False), ('country_ids', 'in', [order.partner_id.country_id.id])]
|
|
])
|
|
acquirers = request.env['payment.acquirer'].sudo().search(domain)
|
|
|
|
values['acquirers'] = acquirers.filtered(lambda acq: (acq.payment_flow == 'form' and acq.view_template_id) or
|
|
(acq.payment_flow == 's2s' and acq.registration_view_template_id))
|
|
values['pms'] = request.env['payment.token'].search([('partner_id', '=', order.partner_id.id)])
|
|
values['acq_extra_fees'] = acquirers.get_acquirer_extra_fees(order.amount_total, order.currency_id, order.partner_id.country_id.id)
|
|
|
|
if order.state in ('draft', 'sent', 'cancel'):
|
|
history = request.session.get('my_quotations_history', [])
|
|
else:
|
|
history = request.session.get('my_orders_history', [])
|
|
values.update(get_records_pager(history, order))
|
|
|
|
return values
|
|
|
|
#
|
|
# Quotations and Sales Orders
|
|
#
|
|
|
|
@http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth="user", website=True)
|
|
def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
|
|
values = self._prepare_portal_layout_values()
|
|
partner = request.env.user.partner_id
|
|
SaleOrder = request.env['sale.order']
|
|
|
|
domain = [
|
|
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
|
('state', 'in', ['sent', 'cancel'])
|
|
]
|
|
|
|
searchbar_sortings = {
|
|
'date': {'label': _('Order Date'), 'order': 'date_order desc'},
|
|
'name': {'label': _('Reference'), 'order': 'name'},
|
|
'stage': {'label': _('Stage'), 'order': 'state'},
|
|
}
|
|
|
|
# default sortby order
|
|
if not sortby:
|
|
sortby = 'date'
|
|
sort_order = searchbar_sortings[sortby]['order']
|
|
|
|
if date_begin and date_end:
|
|
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
|
|
|
# count for pager
|
|
quotation_count = SaleOrder.search_count(domain)
|
|
# make pager
|
|
pager = portal_pager(
|
|
url="/my/quotes",
|
|
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
|
|
total=quotation_count,
|
|
page=page,
|
|
step=self._items_per_page
|
|
)
|
|
# search the count to display, according to the pager data
|
|
quotations = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager['offset'])
|
|
request.session['my_quotations_history'] = quotations.ids[:100]
|
|
|
|
values.update({
|
|
'date': date_begin,
|
|
'quotations': quotations.sudo(),
|
|
'page_name': 'quote',
|
|
'pager': pager,
|
|
'default_url': '/my/quotes',
|
|
'searchbar_sortings': searchbar_sortings,
|
|
'sortby': sortby,
|
|
})
|
|
return request.render("sale.portal_my_quotations", values)
|
|
|
|
@http.route(['/my/orders', '/my/orders/page/<int:page>'], type='http', auth="user", website=True)
|
|
def portal_my_orders(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
|
|
values = self._prepare_portal_layout_values()
|
|
partner = request.env.user.partner_id
|
|
SaleOrder = request.env['sale.order']
|
|
|
|
domain = [
|
|
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
|
('state', 'in', ['sale', 'done'])
|
|
]
|
|
|
|
searchbar_sortings = {
|
|
'date': {'label': _('Order Date'), 'order': 'date_order desc'},
|
|
'name': {'label': _('Reference'), 'order': 'name'},
|
|
'stage': {'label': _('Stage'), 'order': 'state'},
|
|
}
|
|
# default sortby order
|
|
if not sortby:
|
|
sortby = 'date'
|
|
sort_order = searchbar_sortings[sortby]['order']
|
|
|
|
if date_begin and date_end:
|
|
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
|
|
|
# count for pager
|
|
order_count = SaleOrder.search_count(domain)
|
|
# pager
|
|
pager = portal_pager(
|
|
url="/my/orders",
|
|
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
|
|
total=order_count,
|
|
page=page,
|
|
step=self._items_per_page
|
|
)
|
|
# content according to pager
|
|
orders = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager['offset'])
|
|
request.session['my_orders_history'] = orders.ids[:100]
|
|
|
|
values.update({
|
|
'date': date_begin,
|
|
'orders': orders.sudo(),
|
|
'page_name': 'order',
|
|
'pager': pager,
|
|
'default_url': '/my/orders',
|
|
'searchbar_sortings': searchbar_sortings,
|
|
'sortby': sortby,
|
|
})
|
|
return request.render("sale.portal_my_orders", values)
|
|
|
|
@http.route(['/my/orders/<int:order_id>'], type='http', auth="public", website=True)
|
|
def portal_order_page(self, order_id, report_type=None, access_token=None, message=False, download=False, **kw):
|
|
try:
|
|
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
|
except (AccessError, MissingError):
|
|
return request.redirect('/my')
|
|
|
|
if report_type in ('html', 'pdf', 'text'):
|
|
return self._show_report(model=order_sudo, report_type=report_type, report_ref='sale.action_report_saleorder', download=download)
|
|
|
|
# use sudo to allow accessing/viewing orders for public user
|
|
# only if he knows the private token
|
|
# Log only once a day
|
|
if order_sudo:
|
|
# store the date as a string in the session to allow serialization
|
|
now = fields.Date.today().isoformat()
|
|
session_obj_date = request.session.get('view_quote_%s' % order_sudo.id)
|
|
if session_obj_date != now and request.env.user.share and access_token:
|
|
request.session['view_quote_%s' % order_sudo.id] = now
|
|
body = _('Quotation viewed by customer %s', order_sudo.partner_id.name)
|
|
_message_post_helper(
|
|
"sale.order",
|
|
order_sudo.id,
|
|
body,
|
|
token=order_sudo.access_token,
|
|
message_type="notification",
|
|
subtype_xmlid="mail.mt_note",
|
|
partner_ids=order_sudo.user_id.sudo().partner_id.ids,
|
|
)
|
|
|
|
values = self._order_get_page_view_values(order_sudo, access_token, **kw)
|
|
values['message'] = message
|
|
|
|
return request.render('sale.sale_order_portal_template', values)
|
|
|
|
@http.route(['/my/orders/<int:order_id>/accept'], type='json', auth="public", website=True)
|
|
def portal_quote_accept(self, order_id, access_token=None, name=None, signature=None):
|
|
# get from query string if not on json param
|
|
access_token = access_token or request.httprequest.args.get('access_token')
|
|
try:
|
|
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
|
except (AccessError, MissingError):
|
|
return {'error': _('Invalid order.')}
|
|
|
|
if not order_sudo.has_to_be_signed():
|
|
return {'error': _('The order is not in a state requiring customer signature.')}
|
|
if not signature:
|
|
return {'error': _('Signature is missing.')}
|
|
|
|
try:
|
|
order_sudo.write({
|
|
'signed_by': name,
|
|
'signed_on': fields.Datetime.now(),
|
|
'signature': signature,
|
|
})
|
|
request.env.cr.commit()
|
|
except (TypeError, binascii.Error) as e:
|
|
return {'error': _('Invalid signature data.')}
|
|
|
|
if not order_sudo.has_to_be_paid():
|
|
order_sudo.action_confirm()
|
|
order_sudo._send_order_confirmation_mail()
|
|
|
|
pdf = request.env.ref('sale.action_report_saleorder').sudo()._render_qweb_pdf([order_sudo.id])[0]
|
|
|
|
_message_post_helper(
|
|
'sale.order', order_sudo.id, _('Order signed by %s') % (name,),
|
|
attachments=[('%s.pdf' % order_sudo.name, pdf)],
|
|
**({'token': access_token} if access_token else {}))
|
|
|
|
query_string = '&message=sign_ok'
|
|
if order_sudo.has_to_be_paid(True):
|
|
query_string += '#allow_payment=yes'
|
|
return {
|
|
'force_refresh': True,
|
|
'redirect_url': order_sudo.get_portal_url(query_string=query_string),
|
|
}
|
|
|
|
@http.route(['/my/orders/<int:order_id>/decline'], type='http', auth="public", methods=['POST'], website=True)
|
|
def decline(self, order_id, access_token=None, **post):
|
|
try:
|
|
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
|
except (AccessError, MissingError):
|
|
return request.redirect('/my')
|
|
|
|
message = post.get('decline_message')
|
|
|
|
query_string = False
|
|
if order_sudo.has_to_be_signed() and message:
|
|
order_sudo.action_cancel()
|
|
_message_post_helper('sale.order', order_id, message, **{'token': access_token} if access_token else {})
|
|
else:
|
|
query_string = "&message=cant_reject"
|
|
|
|
return request.redirect(order_sudo.get_portal_url(query_string=query_string))
|
|
|
|
# note: website_sale code
|
|
@http.route(['/my/orders/<int:order_id>/transaction/'], type='json', auth="public", website=True)
|
|
def payment_transaction_token(self, acquirer_id, order_id, save_token=False, access_token=None, **kwargs):
|
|
""" Json method that creates a payment.transaction, used to create a
|
|
transaction when the user clicks on 'pay now' button. After having
|
|
created the transaction, the event continues and the user is redirected
|
|
to the acquirer website.
|
|
|
|
:param int acquirer_id: id of a payment.acquirer record. If not set the
|
|
user is redirected to the checkout page
|
|
"""
|
|
# Ensure a payment acquirer is selected
|
|
if not acquirer_id:
|
|
return False
|
|
|
|
try:
|
|
acquirer_id = int(acquirer_id)
|
|
except:
|
|
return False
|
|
|
|
order = request.env['sale.order'].sudo().browse(order_id)
|
|
if not order or not order.order_line or not order.has_to_be_paid():
|
|
return False
|
|
|
|
# Create transaction
|
|
vals = {
|
|
'acquirer_id': acquirer_id,
|
|
'type': order._get_payment_type(save_token),
|
|
'return_url': order.get_portal_url(),
|
|
}
|
|
|
|
transaction = order._create_payment_transaction(vals)
|
|
PaymentProcessing.add_payment_transaction(transaction)
|
|
return transaction.render_sale_button(
|
|
order,
|
|
submit_txt=_('Pay & Confirm'),
|
|
render_values={
|
|
'type': order._get_payment_type(save_token),
|
|
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
|
|
}
|
|
)
|
|
|
|
@http.route('/my/orders/<int:order_id>/transaction/token', type='http', auth='public', website=True)
|
|
def payment_token(self, order_id, pm_id=None, **kwargs):
|
|
|
|
order = request.env['sale.order'].sudo().browse(order_id)
|
|
if not order:
|
|
return request.redirect("/my/orders")
|
|
if not order.order_line or pm_id is None or not order.has_to_be_paid():
|
|
return request.redirect(order.get_portal_url())
|
|
|
|
# try to convert pm_id into an integer, if it doesn't work redirect the user to the quote
|
|
try:
|
|
pm_id = int(pm_id)
|
|
except ValueError:
|
|
return request.redirect(order.get_portal_url())
|
|
|
|
# Create transaction
|
|
vals = {
|
|
'payment_token_id': pm_id,
|
|
'type': 'server2server',
|
|
'return_url': order.get_portal_url(),
|
|
}
|
|
|
|
tx = order._create_payment_transaction(vals)
|
|
PaymentProcessing.add_payment_transaction(tx)
|
|
return request.redirect('/payment/process')
|