[REF] various: move sale_payment and website_quote into sale and sale_management
[MOV] move sale_payment into sale ================================= Almost all use cases of sale require sale_payment down the line. There is only one case which does not: using sale_management without website_sale or website_quote. However, since we want to move most of the features of website_quote into sale_management, moving sale_payement first will make the task easier. [MOV] move website_quote into sale and sale_management ====================================================== We want to use some features of website_quote without having to install the whole module. The main issue with website_quote was that it had a requirement on website. What is common to website_sale and sale_management has been moved into sale. This is primarily the sales order portal view. What is only related to sale_management has been moved into it. This is notably: sales order templates, optional products, sign and pay, ... Everything that requires website has been moved into a new module named sale_quotation_builder. This is related to using the HTML builder to customize the quotation view. Models, files and routes have been renamed to better convey their purpose and to follow guidelines more closely. [IMP] merge sales order portal views ==================================== The old sales order view on the portal has been merged into the one from website_quote. The new view contains features of the two. A lot of fixes and improvements have also been done, related to BS4 issues but also to improve the sales order view in general. [IMP] add new settings ====================== The commit adds new settings related to activating the features that previously were activated by installing website_quote. PR: #26345 Task: 49205, 1838924
@@ -427,8 +427,8 @@ class AccountInvoice(models.Model):
|
||||
|
||||
def _compute_access_url(self):
|
||||
super(AccountInvoice, self)._compute_access_url()
|
||||
for order in self:
|
||||
order.access_url = '/my/invoices/%s' % (order.id)
|
||||
for invoice in self:
|
||||
invoice.access_url = '/my/invoices/%s' % (invoice.id)
|
||||
|
||||
@api.depends('state', 'journal_id', 'date_invoice')
|
||||
def _get_sequence_prefix(self):
|
||||
@@ -1505,6 +1505,24 @@ class AccountInvoice(models.Model):
|
||||
len(res),
|
||||
) for r in res]
|
||||
|
||||
@api.multi
|
||||
def get_portal_url(self, suffix=None):
|
||||
"""
|
||||
Get a portal url for this invoice, including access_token.
|
||||
- suffix: string to append to the url, before the query string
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.access_url + '%s?access_token=%s' % (suffix if suffix else '', self._portal_ensure_token())
|
||||
|
||||
@api.multi
|
||||
def preview_invoice(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'self',
|
||||
'url': self.get_portal_url(),
|
||||
}
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_name = "account.invoice.line"
|
||||
|
||||
@@ -408,6 +408,7 @@
|
||||
string="Register Payment" groups="account.group_account_invoice" class="oe_highlight"/>
|
||||
<button name="action_invoice_open" type="object" states="draft" string="Validate" class="oe_highlight o_invoice_validate" groups="account.group_account_invoice"/>
|
||||
<button name="%(action_account_invoice_refund)d" type='action' string='Add Credit Note' groups="account.group_account_invoice" attrs="{'invisible': ['|',('type', '=', 'out_refund'), ('state', 'not in', ('open','paid'))]}"/>
|
||||
<button name="preview_invoice" type="object" string="Preview"/>
|
||||
<button name="action_invoice_draft" states="cancel" string="Reset to Draft" type="object" groups="account.group_account_invoice"/>
|
||||
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,open,paid"/>
|
||||
</header>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<field name="inherit_id" ref="account.invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_invoice_draft']" position="before">
|
||||
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel Invoice" groups="account.group_account_invoice"/>
|
||||
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -28,7 +28,7 @@
|
||||
<field name="inherit_id" ref="account.invoice_supplier_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_invoice_draft']" position="before">
|
||||
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel Invoice" groups="account.group_account_invoice"/>
|
||||
<button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -18,7 +18,6 @@ var PortalSidebar = Widget.extend({
|
||||
// Window Handlers
|
||||
$(window).on('resize', _.throttle(self._onUpdateSidebarPosition.bind(self), 200, {leading: false}));
|
||||
this._onUpdateSidebarPosition();
|
||||
this._setAffix();
|
||||
this._setDelayLabel();
|
||||
},
|
||||
|
||||
@@ -49,20 +48,6 @@ var PortalSidebar = Widget.extend({
|
||||
$(el).text(displayStr);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* updates affix status of sidebar for window scrolle
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setAffix : function () {
|
||||
var $bsSidebar = this.$el.find('.bs-sidebar');
|
||||
$bsSidebar.affix({
|
||||
offset: {
|
||||
top: 0,
|
||||
bottom: $('#wrapwrap').outerHeight() - $('main').height(),
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} href
|
||||
@@ -97,7 +82,6 @@ var PortalSidebar = Widget.extend({
|
||||
_onUpdateSidebarPosition: function () {
|
||||
var $sidebar = this.$el.find('.bs-sidebar, .o_portal_brand');
|
||||
$sidebar.css({
|
||||
position: config.device.size_class >= config.device.SIZES.MD ? "fixed" : '',
|
||||
width: config.device.size_class >= config.device.SIZES.MD ? $sidebar.outerWidth() : '',
|
||||
});
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ odoo.define('portal.signature_form', function (require){
|
||||
'color': '#000',
|
||||
'background-color': '#fff',
|
||||
'height': '142px',
|
||||
'width': '100%', // prevent the signature from being too big
|
||||
});
|
||||
this.empty_sign = this.$("#o_portal_signature").jSignature('getData', 'image');
|
||||
},
|
||||
@@ -112,6 +113,11 @@ odoo.define('portal.signature_form', function (require){
|
||||
var form = new SignatureForm(null, $elem.data());
|
||||
form.appendTo($elem);
|
||||
});
|
||||
// Make the signature responsive when it is displayed in bootstrap modal.
|
||||
// More precisely it is too small if this code is not here.
|
||||
$('.o_portal_signature_form').parents('.modal').on('shown.bs.modal', function (ev) {
|
||||
$('.o_portal_signature_form').trigger('resize');
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -275,7 +275,6 @@ img.o_we_custom_image {
|
||||
.breadcrumb {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
margin-bottom: 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.record_pager {
|
||||
@@ -383,6 +382,7 @@ img.o_we_custom_image {
|
||||
.o_portal_chatter_avatar{
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.o_portal_chatter_header {
|
||||
@@ -420,7 +420,6 @@ img.o_we_custom_image {
|
||||
|
||||
.o_portal_sidebar {
|
||||
.bs-sidebar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
background-color: $light;
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</t>
|
||||
|
||||
<t t-name="portal.chatter">
|
||||
<div class="o_portal_chatter">
|
||||
<div class="o_portal_chatter p-0">
|
||||
<div class="o_portal_chatter_header">
|
||||
<t t-call="portal.chatter_message_count"/>
|
||||
</div>
|
||||
|
||||
@@ -72,11 +72,11 @@
|
||||
<div class="oe_structure">
|
||||
<div t-if="not no_breadcrumbs" class="row">
|
||||
<div t-attf-class="col-md-12">
|
||||
<t t-call='portal.record_pager'/>
|
||||
<ol t-if="page_name != 'home'" class="breadcrumb o_portal_submenu">
|
||||
<li class="breadcrumb-item"><a href="/my/home" aria-label="Home" title="Home"><i class="fa fa-home"/></a></li>
|
||||
<li t-if="page_name == 'my_details'" class="breadcrumb-item">Your Details</li>
|
||||
</ol>
|
||||
<t t-call='portal.record_pager'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -357,7 +357,7 @@
|
||||
|
||||
<template id="record_pager" name="Portal Record Pager">
|
||||
<t t-if='prev_record or next_record'>
|
||||
<div class="ml8 record_pager btn-group btn-group-xs float-right" role="group">
|
||||
<div class="ml8 record_pager btn-group btn-group-xs float-md-right" role="group">
|
||||
<a role="button" t-att-class="'btn btn-secondary %s' % ('disabled' if not prev_record else '')" t-att-href="prev_record or '#'" ><i class="fa fa-arrow-left" role="img" aria-label="Previous" title="Previous"></i></a>
|
||||
<a role="button" t-att-class="'btn btn-secondary %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-arrow-right" role="img" aria-label="Next" title="Previous"></i></a>
|
||||
</div>
|
||||
@@ -395,7 +395,7 @@
|
||||
the message will be posted with the identity of the partner_id of the object
|
||||
-->
|
||||
<template id="message_thread">
|
||||
<div id="discussion" class="d-print-none o_portal_chatter o_not_editable"
|
||||
<div id="discussion" class="d-print-none o_portal_chatter o_not_editable p-0"
|
||||
t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-pid="pid" t-att-data-hash="hash" t-att-data-res_id="object.id" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
'description': """
|
||||
This module contains all the common features of Sales Management and eCommerce.
|
||||
""",
|
||||
'depends': ['sales_team', 'account', 'portal'],
|
||||
'depends': ['sales_team', 'payment', 'portal'],
|
||||
'data': [
|
||||
'security/sale_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
@@ -31,6 +31,7 @@ This module contains all the common features of Sales Management and eCommerce.
|
||||
'views/sale_onboarding_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/payment_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/sale_demo.xml',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import http, _
|
||||
from odoo import fields, http, _
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.mail import _message_post_helper
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager, get_records_pager
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
@@ -36,15 +35,6 @@ class CustomerPortal(CustomerPortal):
|
||||
# Quotations and Sales Orders
|
||||
#
|
||||
|
||||
def _order_get_page_view_values(self, order, access_token, **kwargs):
|
||||
order_invoice_lines = {il.product_id.id: il.invoice_id for il in order.invoice_ids.mapped('invoice_line_ids')}
|
||||
values = {
|
||||
'order': order,
|
||||
'order_invoice_lines': order_invoice_lines,
|
||||
'portal_confirmation': order.get_portal_confirmation_action(),
|
||||
}
|
||||
return self._get_page_view_values(order, access_token, values, 'my_orders_history', True, **kwargs)
|
||||
|
||||
@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()
|
||||
@@ -149,66 +139,59 @@ class CustomerPortal(CustomerPortal):
|
||||
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=None, access_token=None, **kw):
|
||||
def portal_order_page(self, order_id, pdf=None, access_token=None, message=False, **kw):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except AccessError:
|
||||
return request.redirect('/my')
|
||||
|
||||
values = self._order_get_page_view_values(order_sudo, access_token, **kw)
|
||||
return request.render("sale.portal_order_page", values)
|
||||
if pdf:
|
||||
pdf = request.env.ref('sale.report_web_quote').sudo().with_context(set_viewport_size=True).render_qweb_pdf([order_sudo.id])[0]
|
||||
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
|
||||
return request.make_response(pdf, headers=pdfhttpheaders)
|
||||
|
||||
@http.route(['/my/orders/pdf/<int:order_id>'], type='http', auth="public", website=True)
|
||||
def portal_order_report(self, order_id, access_token=None, **kw):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token)
|
||||
except AccessError:
|
||||
# use sudo to allow accessing/viewing orders for public user
|
||||
# only if he knows the private token
|
||||
now = fields.Date.today()
|
||||
|
||||
# Log only once a day
|
||||
if order_sudo and request.session.get('view_quote_%s' % order_sudo.id) != now and request.env.user.share and access_token:
|
||||
request.session['view_quote_%s' % order_sudo.id] = now
|
||||
body = _('Quotation viewed by customer')
|
||||
_message_post_helper(res_model='sale.order', res_id=order_sudo.id, message=body, token=order_sudo.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=order_sudo.user_id.sudo().partner_id.ids)
|
||||
if not order_sudo:
|
||||
return request.redirect('/my')
|
||||
|
||||
# print report as sudo, since it require access to taxes, payment term, ... and portal
|
||||
# does not have those access rights.
|
||||
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
|
||||
pdfhttpheaders = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(pdf)),
|
||||
]
|
||||
return request.make_response(pdf, headers=pdfhttpheaders)
|
||||
transaction = order_sudo.get_portal_last_transaction()
|
||||
|
||||
def _portal_quote_user_can_accept(self, order):
|
||||
return order.company_id.portal_confirmation_sign or order.company_id.portal_confirmation_pay
|
||||
|
||||
@http.route(['/my/quotes/accept'], type='json', auth="public", website=True)
|
||||
def portal_quote_accept(self, res_id, access_token=None, partner_name=None, signature=None):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', res_id, access_token=access_token)
|
||||
except AccessError:
|
||||
return {'error': _('Invalid order')}
|
||||
if order_sudo.state != 'sent':
|
||||
return {'error': _('Order is not in a state requiring customer validation.')}
|
||||
|
||||
if not self._portal_quote_user_can_accept(order_sudo):
|
||||
return {'error': _('Operation not allowed')}
|
||||
if not signature:
|
||||
return {'error': _('Signature is missing.')}
|
||||
|
||||
success_message = _('Your order has been signed but still needs to be paid to be confirmed.')
|
||||
|
||||
if not order_sudo.has_to_be_paid():
|
||||
order_sudo.action_confirm()
|
||||
success_message = _('Your order has been confirmed.')
|
||||
|
||||
order_sudo.signature = signature
|
||||
order_sudo.signed_by = partner_name
|
||||
|
||||
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
|
||||
_message_post_helper(
|
||||
res_model='sale.order',
|
||||
res_id=order_sudo.id,
|
||||
message=_('Order signed by %s') % (partner_name,),
|
||||
attachments=[('%s.pdf' % order_sudo.name, pdf)],
|
||||
**({'token': access_token} if access_token else {}))
|
||||
|
||||
return {
|
||||
'success': success_message,
|
||||
'redirect_url': '/my/orders/%s?%s' % (order_sudo.id, access_token and 'access_token=%s' % order_sudo.access_token or ''),
|
||||
# TODO SEB fix breadcrumbs -> should always be visible (or at least no home alone)
|
||||
values = {
|
||||
'sale_order': order_sudo,
|
||||
'message': int(message) if message else False,
|
||||
'action': request.env.ref('sale.action_quotations').id,
|
||||
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
|
||||
'tx_state': transaction.state if transaction else False,
|
||||
'need_payment': order_sudo.invoice_status == 'to invoice' and transaction.state in ['draft', 'cancel'],
|
||||
'token': access_token,
|
||||
'return_url': '/shop/payment/validate',
|
||||
'bootstrap_formatting': True,
|
||||
'partner_id': order_sudo.partner_id.id,
|
||||
}
|
||||
|
||||
if order_sudo.has_to_be_paid() or values['need_payment']:
|
||||
domain = expression.AND([
|
||||
['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)],
|
||||
['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])]
|
||||
])
|
||||
acquirers = request.env['payment.acquirer'].sudo().search(domain)
|
||||
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
|
||||
values['pms'] = request.env['payment.token'].search(
|
||||
[('partner_id', '=', order_sudo.partner_id.id),
|
||||
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
|
||||
|
||||
history = request.session.get('my_quotes_history', [])
|
||||
values.update(get_records_pager(history, order_sudo))
|
||||
|
||||
return request.render('sale.sale_order_portal_template', values)
|
||||
|
||||
@@ -12,3 +12,4 @@ from . import sale
|
||||
from . import res_config_settings
|
||||
from . import sales_team
|
||||
from . import res_users
|
||||
from . import payment
|
||||
|
||||
@@ -66,7 +66,7 @@ class PaymentTransaction(models.Model):
|
||||
sales_orders.force_quotation_send()
|
||||
sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'sent')
|
||||
sales_orders.action_confirm()
|
||||
automatic_invoice = self.env['ir.config_parameter'].sudo().get_param('sale_payment.automatic_invoice')
|
||||
automatic_invoice = self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice')
|
||||
if automatic_invoice:
|
||||
for trans in self.filtered(lambda t: t.sale_order_ids):
|
||||
trans.sale_order_ids._force_lines_to_invoice_policy_order()
|
||||
@@ -74,7 +74,7 @@ class PaymentTransaction(models.Model):
|
||||
trans.invoice_ids = [(6, 0, invoices)]
|
||||
res = super(PaymentTransaction, self)._set_transaction_done()
|
||||
if automatic_invoice:
|
||||
default_template = self.env['ir.config_parameter'].sudo().get_param('sale_payment.default_email_template')
|
||||
default_template = self.env['ir.config_parameter'].sudo().get_param('sale.default_email_template')
|
||||
if default_template:
|
||||
for invoice in trans.invoice_ids:
|
||||
invoice.with_context(mark_invoice_as_sent=True).message_post_with_template(int(default_template), notif_layout="mail.mail_notification_paynow")
|
||||
@@ -8,8 +8,9 @@ class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
sale_note = fields.Text(string='Default Terms and Conditions', translate=True)
|
||||
portal_confirmation_sign = fields.Boolean(string='Digital Signature')
|
||||
portal_confirmation_sign = fields.Boolean(string='Online Signature')
|
||||
portal_confirmation_pay = fields.Boolean(string='Electronic Payment')
|
||||
quotation_validity_days = fields.Integer(default=30, string="Default Quotation Validity (Days)")
|
||||
|
||||
# sale quotation onboarding
|
||||
sale_quotation_onboarding_state = fields.Selection([('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done"), ('closed', "Closed")], string="State of the sale onboarding panel", default='not_done')
|
||||
@@ -108,3 +109,5 @@ class ResCompany(models.Model):
|
||||
action = self.env.ref('sale.action_open_sale_onboarding_quotation_layout').read()[0]
|
||||
action['res_id'] = self.env.user.company_id.id
|
||||
return action
|
||||
|
||||
_sql_constraints = [('check_quotation_validity_days', 'CHECK(quotation_validity_days > 0)', 'Quotation Validity is required and must be greater than 0.')]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
@@ -16,11 +14,11 @@ class ResConfigSettings(models.TransientModel):
|
||||
config_parameter='sale.use_sale_note')
|
||||
group_discount_per_so_line = fields.Boolean("Discounts", implied_group='sale.group_discount_per_so_line')
|
||||
module_sale_margin = fields.Boolean("Margins")
|
||||
quotation_validity_days = fields.Integer(related='company_id.quotation_validity_days', string="Default Quotation Validity (Days)")
|
||||
use_quotation_validity_days = fields.Boolean("Default Quotation Validity", config_parameter='sale.use_quotation_validity_days')
|
||||
group_warning_sale = fields.Boolean("Sale Order Warnings", implied_group='sale.group_warning_sale')
|
||||
portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Digital Signature')
|
||||
portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Online Signature')
|
||||
portal_confirmation_pay = fields.Boolean(related='company_id.portal_confirmation_pay', string='Electronic Payment')
|
||||
module_sale_payment = fields.Boolean("Sale Payment", help='Technical field implied by user choice of portal_confirmation_pay.')
|
||||
module_website_quote = fields.Boolean("Quotations Templates")
|
||||
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='sale.group_delivery_invoice_address')
|
||||
multi_sales_price = fields.Boolean("Multiple Sales Prices per Product")
|
||||
multi_sales_price_method = fields.Selection([
|
||||
@@ -67,6 +65,18 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_product_email_template = fields.Boolean("Specific Email")
|
||||
module_sale_coupon = fields.Boolean("Coupons & Promotions")
|
||||
|
||||
automatic_invoice = fields.Boolean("Automatic Invoice",
|
||||
help="The invoice is generated automatically and available in the customer portal "
|
||||
"when the transaction is confirmed by the payment acquirer.\n"
|
||||
"The invoice is marked as paid and the payment is registered in the payment journal "
|
||||
"defined in the configuration of the payment acquirer.\n"
|
||||
"This mode is advised if you issue the final invoice at the order and not after the delivery.",
|
||||
config_parameter='sale.automatic_invoice')
|
||||
template_id = fields.Many2one('mail.template', 'Email Template',
|
||||
domain="[('model', '=', 'account.invoice')]",
|
||||
config_parameter='sale.default_email_template',
|
||||
default=lambda self: self.env.ref('account.email_template_edi_invoice', False))
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
if not self.group_discount_per_so_line:
|
||||
@@ -107,6 +117,19 @@ class ResConfigSettings(models.TransientModel):
|
||||
if self.portal_confirmation_pay:
|
||||
self.module_sale_payment = True
|
||||
|
||||
@api.onchange('use_quotation_validity_days')
|
||||
def _onchange_use_quotation_validity_days(self):
|
||||
if self.quotation_validity_days <= 0:
|
||||
self.quotation_validity_days = self.env['res.company'].default_get(['quotation_validity_days'])['quotation_validity_days']
|
||||
|
||||
@api.onchange('quotation_validity_days')
|
||||
def _onchange_quotation_validity_days(self):
|
||||
if self.quotation_validity_days <= 0:
|
||||
self.quotation_validity_days = self.env['res.company'].default_get(['quotation_validity_days'])['quotation_validity_days']
|
||||
return {
|
||||
'warning': {'title': "Warning", 'message': "Quotation Validity is required and must be greater than 0."},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
@@ -23,6 +21,13 @@ class SaleOrder(models.Model):
|
||||
_description = "Quotation"
|
||||
_order = 'date_order desc, id desc'
|
||||
|
||||
def _default_validity_date(self):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('sale.use_quotation_validity_days'):
|
||||
days = self.env.user.company_id.quotation_validity_days
|
||||
if days > 0:
|
||||
return fields.Date.to_string(datetime.now() + timedelta(days))
|
||||
return False
|
||||
|
||||
@api.depends('order_line.price_total')
|
||||
def _amount_all(self):
|
||||
"""
|
||||
@@ -121,9 +126,10 @@ class SaleOrder(models.Model):
|
||||
('cancel', 'Cancelled'),
|
||||
], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', track_sequence=3, default='draft')
|
||||
date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now)
|
||||
validity_date = fields.Date(string='Quote Validity', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
help="Validity date of the quotation, after this date, the customer won't be able to validate the quotation online.")
|
||||
validity_date = fields.Date(string='Validity', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
help="Validity date of the quotation, after this date, the customer won't be able to validate the quotation online.", default=_default_validity_date)
|
||||
is_expired = fields.Boolean(compute='_compute_is_expired', string="Is expired")
|
||||
remaining_validity_days = fields.Integer(compute='_compute_remaining_validity_days', string="Remaining Validity Days")
|
||||
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
|
||||
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sales order is confirmed.", oldname="date_confirm", copy=False)
|
||||
user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', track_sequence=2, default=lambda self: self.env.user)
|
||||
@@ -168,6 +174,12 @@ class SaleOrder(models.Model):
|
||||
"will be scheduled based on this date rather than product lead times.")
|
||||
expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=False, oldname='commitment_date', # Note: can not be stored since depends on today()
|
||||
help="Delivery date you can promise to the customer, computed from product lead times and from the shipping policy of the order.")
|
||||
amount_undiscounted = fields.Float('Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
|
||||
|
||||
transaction_ids = fields.Many2many('payment.transaction', 'sale_order_transaction_rel', 'sale_order_id', 'transaction_id',
|
||||
string='Transactions', copy=False, readonly=True)
|
||||
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
|
||||
string='Authorized Transactions', copy=False, readonly=True)
|
||||
|
||||
def _compute_access_url(self):
|
||||
super(SaleOrder, self)._compute_access_url()
|
||||
@@ -197,9 +209,25 @@ class SaleOrder(models.Model):
|
||||
if dates_list:
|
||||
order.expected_date = fields.Datetime.to_string(min(dates_list))
|
||||
|
||||
@api.model
|
||||
def _get_customer_lead(self, product_tmpl_id):
|
||||
return False
|
||||
@api.multi
|
||||
def _compute_remaining_validity_days(self):
|
||||
for record in self:
|
||||
if record.validity_date:
|
||||
record.remaining_validity_days = (record.validity_date - fields.Date.today()).days + 1
|
||||
else:
|
||||
record.remaining_validity_days = 0
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_authorized_transaction_ids(self):
|
||||
for trans in self:
|
||||
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
|
||||
|
||||
@api.one
|
||||
def _compute_amount_undiscounted(self):
|
||||
total = 0.0
|
||||
for line in self.order_line:
|
||||
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ??
|
||||
self.amount_undiscounted = total
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
@@ -401,7 +429,8 @@ class SaleOrder(models.Model):
|
||||
'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
'user_id': self.user_id and self.user_id.id,
|
||||
'team_id': self.team_id.id
|
||||
'team_id': self.team_id.id,
|
||||
'transaction_ids': [(6, 0, self.transaction_ids.ids)],
|
||||
}
|
||||
return invoice_vals
|
||||
|
||||
@@ -694,6 +723,104 @@ class SaleOrder(models.Model):
|
||||
|
||||
return groups
|
||||
|
||||
@api.multi
|
||||
def _create_payment_transaction(self, vals):
|
||||
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
|
||||
current sales orders fields (e.g. the partner or the currency).
|
||||
:param vals: The values to create a new payment.transaction.
|
||||
:return: The newly created payment.transaction record.
|
||||
'''
|
||||
# Ensure the currencies are the same.
|
||||
currency = self[0].pricelist_id.currency_id
|
||||
if any([so.pricelist_id.currency_id != currency for so in self]):
|
||||
raise ValidationError(_('A transaction can\'t be linked to sales orders having different currencies.'))
|
||||
|
||||
# Ensure the partner are the same.
|
||||
partner = self[0].partner_id
|
||||
if any([so.partner_id != partner for so in self]):
|
||||
raise ValidationError(_('A transaction can\'t be linked to sales orders having different partners.'))
|
||||
|
||||
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
|
||||
acquirer_id = vals.get('acquirer_id')
|
||||
acquirer = False
|
||||
payment_token_id = vals.get('payment_token_id')
|
||||
|
||||
if payment_token_id:
|
||||
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
|
||||
|
||||
# Check payment_token/acquirer matching or take the acquirer from token
|
||||
if acquirer_id:
|
||||
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
|
||||
if payment_token and payment_token.acquirer_id != acquirer:
|
||||
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
|
||||
payment_token.acquirer_id.name, acquirer.name))
|
||||
if payment_token and payment_token.partner_id != partner:
|
||||
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
|
||||
payment_token.partner.name, partner.name))
|
||||
else:
|
||||
acquirer = payment_token.acquirer_id
|
||||
|
||||
# Check an acquirer is there.
|
||||
if not acquirer_id and not acquirer:
|
||||
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
|
||||
|
||||
if not acquirer:
|
||||
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
|
||||
|
||||
# Check a journal is set on acquirer.
|
||||
if not acquirer.journal_id:
|
||||
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
|
||||
|
||||
if not acquirer_id and acquirer:
|
||||
vals['acquirer_id'] = acquirer.id
|
||||
|
||||
vals.update({
|
||||
'amount': sum(self.mapped('amount_total')),
|
||||
'currency_id': currency.id,
|
||||
'partner_id': partner.id,
|
||||
'sale_order_ids': [(6, 0, self.ids)],
|
||||
})
|
||||
|
||||
transaction = self.env['payment.transaction'].create(vals)
|
||||
|
||||
# Process directly if payment_token
|
||||
if transaction.payment_token_id:
|
||||
transaction.s2s_do_transaction()
|
||||
|
||||
return transaction
|
||||
|
||||
def _force_lines_to_invoice_policy_order(self):
|
||||
for line in self.order_line:
|
||||
if self.state in ['sale', 'done']:
|
||||
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
||||
else:
|
||||
line.qty_to_invoice = 0
|
||||
|
||||
@api.multi
|
||||
def payment_action_capture(self):
|
||||
self.authorized_transaction_ids.s2s_capture_transaction()
|
||||
|
||||
@api.multi
|
||||
def payment_action_void(self):
|
||||
self.authorized_transaction_ids.s2s_void_transaction()
|
||||
|
||||
@api.multi
|
||||
def get_portal_url(self, suffix=None):
|
||||
"""
|
||||
Get a portal url for this sale order, including access_token.
|
||||
- suffix: string to append to the url, before the query string
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.access_url + '%s?access_token=%s' % (suffix if suffix else '', self._portal_ensure_token())
|
||||
|
||||
@api.multi
|
||||
def get_portal_last_transaction(self):
|
||||
self.ensure_one()
|
||||
return self.transaction_ids.get_last_transaction()
|
||||
|
||||
@api.model
|
||||
def _get_customer_lead(self, product_tmpl_id):
|
||||
return False
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_name = 'sale.order.line'
|
||||
|
||||
@@ -21,5 +21,18 @@
|
||||
name="sale.report_saleorder_pro_forma"
|
||||
print_report_name="'PRO-FORMA - %s' % (object.name)"
|
||||
/>
|
||||
|
||||
<report
|
||||
id="report_web_quote"
|
||||
string="Web Quotation"
|
||||
model="sale.order"
|
||||
report_type="qweb-pdf"
|
||||
file="sale.report_quotation"
|
||||
name="sale.report_quotation"
|
||||
menu="False"
|
||||
print_report_name="
|
||||
(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name or
|
||||
'Order - %s' % (object.name))"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -254,4 +254,19 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_quotation">
|
||||
<t t-call="web.html_container">
|
||||
<t t-set="data_report_margin_top" t-value="45"/>
|
||||
<t t-set="data_report_header_spacing" t-value="40"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="o" t-value="docs[0]"/>
|
||||
<t t-foreach="docs" t-as="sale_order">
|
||||
<div class="page">
|
||||
<t t-call="sale.sale_order_portal_content"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
odoo.define('sale.SalePortalSidebar.instance', function (require) {
|
||||
"use strict";
|
||||
|
||||
require('web.dom_ready');
|
||||
var SalePortalSidebar = require('sale.SalePortalSidebar');
|
||||
|
||||
if (!$('.o_portal_sale_sidebar').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.o_portal_sale_sidebar'");
|
||||
}
|
||||
|
||||
var $spyWatch = $('body[data-target=".navspy"]'),
|
||||
sale_portal_sidebar = new SalePortalSidebar($spyWatch);
|
||||
|
||||
return sale_portal_sidebar.attachTo($('.o_portal_sale_sidebar')).then(function () {
|
||||
return sale_portal_sidebar;
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
|
||||
odoo.define('sale.SalePortalSidebar', function (require) {
|
||||
"use strict";
|
||||
|
||||
var PortalSidebar = require('portal.PortalSidebar');
|
||||
|
||||
var SalePortalSidebar = PortalSidebar.extend({
|
||||
events: {
|
||||
'click .o_portal_sale_print': '_onPrintSaleOrder',
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} $watched_selector
|
||||
*/
|
||||
init: function ($watched_selector) {
|
||||
this._super.apply(this, arguments);
|
||||
this.authorizedTextTag = ['em', 'b', 'i', 'u'];
|
||||
this.spyWatched = $watched_selector;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
var $spyWatcheElement = this.$el.find('[data-id="portal_sidebar"]');
|
||||
this._setElementId($spyWatcheElement);
|
||||
// Nav Menu ScrollSpy
|
||||
this._generateMenu();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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');
|
||||
|
||||
$("[id^=quote_header_], [id^=quote_]", this.spyWatched).attr("id", "");
|
||||
_.each(this.spyWatched.find("h1, h2"), function (el) {
|
||||
var id, text;
|
||||
switch (el.tagName.toLowerCase()) {
|
||||
case "h1":
|
||||
id = self._setElementId('quote_header_', el);
|
||||
text = self._extractText($(el));
|
||||
if (!text) {
|
||||
break;
|
||||
}
|
||||
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
|
||||
lastUL = false;
|
||||
break;
|
||||
case "h2":
|
||||
id = self._setElementId('quote_', el);
|
||||
text = self._extractText($(el));
|
||||
if (!text) {
|
||||
break;
|
||||
}
|
||||
if (lastLI) {
|
||||
if (!lastUL) {
|
||||
lastUL = $("<ul class='nav'>").appendTo(lastLI);
|
||||
}
|
||||
$("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo(lastUL);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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(' ');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onPrintSaleOrder: function (ev) {
|
||||
ev.preventDefault();
|
||||
var href = $(ev.currentTarget).attr('href');
|
||||
this._printIframeContent(href);
|
||||
},
|
||||
});
|
||||
|
||||
return SalePortalSidebar;
|
||||
});
|
||||
@@ -8,3 +8,12 @@
|
||||
.orders_label_text_align {
|
||||
vertical-align: 15%;
|
||||
}
|
||||
|
||||
tr.o_is_line_note {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
tr.o_is_line_section {
|
||||
font-weight: bold;
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="transaction_form_inherit_sale_payment" model="ir.ui.view">
|
||||
<record id="transaction_form_inherit_sale" model="ir.ui.view">
|
||||
<field name="name">payment.transaction.form.inherit.sale.payment</field>
|
||||
<field name="model">payment.transaction</field>
|
||||
<field name="inherit_id" ref="payment.transaction_form"/>
|
||||
@@ -147,7 +147,7 @@
|
||||
<label for="portal_confirmation_sign"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
|
||||
<div class="text-muted">
|
||||
Request a digital signature to confirm orders
|
||||
Request an online signature to confirm orders
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,22 +156,13 @@
|
||||
<field name="portal_confirmation_pay"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<field name="module_sale_payment" invisible="1"/>
|
||||
<label for="portal_confirmation_pay"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
|
||||
<div class="text-muted">
|
||||
Request an electronic payment to confirm orders
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_website_quote"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_website_quote"/>
|
||||
<div class="text-muted" id="sale_config_website_quote">
|
||||
Design standardized offers
|
||||
<div attrs="{'invisible': [('portal_confirmation_pay', '=', False)]}">
|
||||
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,6 +194,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_quotation_validity_days"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_quotation_validity_days"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
||||
<div class="text-muted">
|
||||
Set a default validity on your quotations
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('use_quotation_validity_days','=',False)]}">
|
||||
<div class="mt16">
|
||||
<span class="col-lg-3">Default Limit: <field name="quotation_validity_days" attrs="{'required': [('use_quotation_validity_days', '=', True)]}"/> days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_warning_sale"/>
|
||||
@@ -363,6 +371,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box"
|
||||
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="automatic_invoice"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="automatic_invoice"/>
|
||||
<div class="text-muted">
|
||||
Generate the invoice automatically when the electronic payment is confirmed
|
||||
</div>
|
||||
<div attrs="{'invisible': [('automatic_invoice','=',False)]}">
|
||||
<label for="template_id" class="o_light_label"/>
|
||||
<field name="template_id" class="oe_inline" options="{'no_create': True}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" groups="base.group_no_one">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Sale frontend assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/sale/static/src/js/sale_portal_sidebar.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" name="project assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/sale/static/src/js/tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_home_menu_sale" name="Portal layout : sales menu entries" inherit_id="portal.portal_layout" priority="20">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'quote' or sales_order and sales_order.state in ('sent', 'cancel')">
|
||||
<li t-if="page_name == 'quote' or sales_order and sales_order.state in ('sent', 'cancel')" class="breadcrumb-item">
|
||||
<a t-if="sales_order" t-attf-href="/my/quotes?{{ keep_query() }}">Quotations</a>
|
||||
<t t-else="">Quotations</t>
|
||||
</li>
|
||||
<li t-if="page_name == 'order' or sales_order and sales_order.state not in ('sent', 'cancel')">
|
||||
<li t-if="page_name == 'order' or sales_order and sales_order.state not in ('sent', 'cancel')" class="breadcrumb-item">
|
||||
<a t-if="sales_order" t-attf-href="/my/orders?{{ keep_query() }}">Sales Orders</a>
|
||||
<t t-else="">Sales Orders</t>
|
||||
</li>
|
||||
<li t-if="sales_order">
|
||||
<li t-if="sales_order" class="breadcrumb-item">
|
||||
<t t-if="sales_order.state in ('sent', 'cancel')">Quotation</t>
|
||||
<t t-else="">Order</t>
|
||||
<t t-esc="sales_order.name"/>
|
||||
@@ -31,369 +45,609 @@
|
||||
</template>
|
||||
|
||||
<template id="portal_my_quotations" name="My Quotations">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Quotations</t>
|
||||
</t>
|
||||
<t t-if="not quotations">
|
||||
<p>There are currently no quotations for your account.</p>
|
||||
</t>
|
||||
<div t-if="quotations" class="card">
|
||||
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Quotation #</th>
|
||||
<th>Order Date</th>
|
||||
<th>Valid Until</th>
|
||||
<th></th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="quotations" t-as="quotation">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/my/orders/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
|
||||
</td>
|
||||
<td><span t-field="quotation.date_order"/></td>
|
||||
<td><span t-field="quotation.validity_date"/></td>
|
||||
<td>
|
||||
<t t-if="quotation.state == 'cancel'">
|
||||
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
|
||||
</t>
|
||||
<t t-if="quotation.is_expired">
|
||||
<span class="badge badge-secondary"><i class="fa fa-fw fa-clock-o"/> Expired</span>
|
||||
</t>
|
||||
</td>
|
||||
<td><span t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table></div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="portal.pager"/>
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Quotations</t>
|
||||
</t>
|
||||
<t t-if="not quotations">
|
||||
<p>There are currently no quotations for your account.</p>
|
||||
</t>
|
||||
<div t-if="quotations" class="card">
|
||||
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Quotation #</th>
|
||||
<th>Order Date</th>
|
||||
<th>Valid Until</th>
|
||||
<th></th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="quotations" t-as="quotation">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-att-href="quotation.get_portal_url()"><t t-esc="quotation.name"/></a>
|
||||
</td>
|
||||
<td><span t-field="quotation.date_order"/></td>
|
||||
<td><span t-field="quotation.validity_date"/></td>
|
||||
<td>
|
||||
<t t-if="quotation.state == 'cancel'">
|
||||
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
|
||||
</t>
|
||||
<t t-if="quotation.is_expired">
|
||||
<span class="badge badge-secondary"><i class="fa fa-fw fa-clock-o"/> Expired</span>
|
||||
</t>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table></div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="portal.pager"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_orders" name="My Sales Orders">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Sales Orders</t>
|
||||
</t>
|
||||
<t t-if="not orders">
|
||||
<p>There are currently no orders for your account.</p>
|
||||
</t>
|
||||
<div t-if="orders" class="card">
|
||||
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>
|
||||
<span class='d-none d-md-inline'>Sales Order #</span>
|
||||
<span class='d-block d-md-none'>Ref.</span>
|
||||
</th>
|
||||
<th>Order Date</th>
|
||||
<th ></th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="orders" t-as="order">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/my/orders/{{order.id}}?{{keep_query()}}"><t t-esc="order.name"/></a>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="order.date_order" t-options="{'widget': 'date'}"/>&nbsp;
|
||||
<span class='d-none d-md-inline' t-field="order.date_order" t-options="{'time_only': True}"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="order.state == 'done'">
|
||||
<span class="badge badge-success d-none d-md-inline-block"><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>
|
||||
</t>
|
||||
</td>
|
||||
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table></div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="portal.pager"/>
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Sales Orders</t>
|
||||
</t>
|
||||
<t t-if="not orders">
|
||||
<p>There are currently no orders for your account.</p>
|
||||
</t>
|
||||
<div t-if="orders" class="card">
|
||||
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>
|
||||
<span class='d-none d-md-inline'>Sales Order #</span>
|
||||
<span class='d-block d-md-none'>Ref.</span>
|
||||
</th>
|
||||
<th>Order Date</th>
|
||||
<th ></th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="orders" t-as="order">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="quotation.get_portal_url()"><t t-esc="order.name"/></a>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="order.date_order" t-options="{'widget': 'date'}"/>&nbsp;
|
||||
<span class='d-none d-md-inline' t-field="order.date_order" t-options="{'time_only': True}"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="order.state == 'done'">
|
||||
<span class="badge badge-success d-none d-md-inline-block"><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>
|
||||
</t>
|
||||
</td>
|
||||
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table></div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="portal.pager"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_page" name="Sales Order">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="sales_order" t-value="order"/> <!-- rename to ensure breadcrumb compatibility (differentiate with purchase orders) -->
|
||||
<t t-set="invoices" t-value="[i for i in order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
|
||||
<t t-set="invoices_amount" t-value="sum(i.amount_total for i in invoices)"/>
|
||||
<div id="optional_placeholder"></div>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-9 col-md-9">
|
||||
<h4>
|
||||
<t t-if="order.state in ('sent', 'cancel')">Quotation</t>
|
||||
<t t-else="">Order</t>
|
||||
<span t-esc="order.name"/>
|
||||
<t t-if="order.state == 'cancel'">
|
||||
<span class="badge badge-secondary"><i class="fa fa-fw fa-remove"/> Cancelled</span>
|
||||
<!-- Complete page of the sale_order -->
|
||||
<template id="sale_order_portal_template" name="Sales Order Portal Template" inherit_id="portal.portal_sidebar" primary="True">
|
||||
<xpath expr="//div[hasclass('o_portal_sidebar')]" position="inside">
|
||||
<div class="row mt16 o_portal_sale_sidebar">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-3 d-print-none">
|
||||
<div class="bs-sidebar">
|
||||
<!-- TODO SEB add sale order total here, like invoice for the style -->
|
||||
<t t-if="sale_order.team_id.team_type != 'website'">
|
||||
<div class="text-center" t-if="sale_order.state in ('draft', 'sent', 'waiting_date')" style="padding: 10px">
|
||||
<t t-set="show_hr" t-value="False"/>
|
||||
<a role="button" t-if="not sale_order.is_expired and not sale_order.signature and sale_order.has_to_be_signed()" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
|
||||
<i class="fa fa-check"/> Accept
|
||||
<t t-set="show_hr" t-value="True"/>
|
||||
</a>
|
||||
<a role="button" t-elif="not sale_order.is_expired and sale_order.has_to_be_paid()" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
|
||||
<i class="fa fa-check"/> <t t-if="not sale_order.signature">Accept & </t>Pay
|
||||
<t t-set="show_hr" t-value="True"/>
|
||||
</a>
|
||||
<a role="button" t-if="sale_order.is_expired" href="#discussion" class="btn btn-info btn-block o_normalwhitespace">
|
||||
<strong>This offer expired!</strong><br/>
|
||||
Contact us for new quotation.
|
||||
<t t-set="show_hr" t-value="True"/>
|
||||
</a>
|
||||
<hr class="mb0" t-if="show_hr"/>
|
||||
</div>
|
||||
<div class="text-center" t-if="need_payment or (sale_order.state in ('sale') and sale_order.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
|
||||
<a role="button" class="btn btn-success btn-block" data-toggle="modal" data-target="#modalaccept" href="#">
|
||||
<i class="fa fa-check"></i> Pay Now
|
||||
</a>
|
||||
<hr class="mb0"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="order.state == 'done'">
|
||||
<span class="badge badge-success"><i class="fa fa-fw fa-check" /> Done</span>
|
||||
</t>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-3 col-3 d-print-none" id="o_portal_order_actions">
|
||||
<t t-if="order.state == 'sent' and portal_confirmation == 'sign'" name="portal_confirmation_sign">
|
||||
<a role="button" class="btn btn-primary ml8 float-right" href="#portal_sign">
|
||||
<i class="fa fa-arrow-circle-right"/> Accept & Sign
|
||||
</a>
|
||||
</t>
|
||||
<t t-if="order.state == 'sale' and portal_confirmation == 'sign'">
|
||||
<a role="button" class="btn btn-success ml8 float-right" disabled="disabled"
|
||||
t-attf-title="Order signed on #{order.confirmation_date} by #{order.partner_id.name}">
|
||||
<i class="fa fa-check-circle"/> Signed
|
||||
</a>
|
||||
</t>
|
||||
<a target="_blank" t-att-href="'/my/orders/pdf/%s?%s' % (order.id, keep_query())" class="ml8 float-right mt4 mr8" title="Download">
|
||||
<i class="fa fa-download fa-2x" role="img" aria-label="Download"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<t t-if="error or warning" t-call="sale.portal_order_error"/>
|
||||
<t t-if="success and (not error and not warning)" t-call="sale.portal_order_success"/>
|
||||
<div class="mb8">
|
||||
<strong>Date:</strong> <span t-field="order.create_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div class="mb8" t-if="order.validity_date">
|
||||
<strong>Expiration Date:</strong> <span t-field="order.validity_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div class='row'>
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<strong>Invoicing Address</strong>
|
||||
</div>
|
||||
<div>
|
||||
<address t-field="order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
|
||||
</div>
|
||||
<t t-if="invoices">
|
||||
<div>
|
||||
<strong>Invoices</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-foreach="invoices" t-as="i">
|
||||
<t t-set="report_url" t-value="'/my/invoices/pdf/%s?%s' % (i.id, 'access_token=%s' % i.access_token if access_token else '')"/>
|
||||
<div>
|
||||
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
|
||||
<a t-att-href="report_url"><span t-esc="i.number"/></a>
|
||||
<span class="text-muted" t-field="i.date_invoice"/>
|
||||
<t t-if="i.state == 'paid'">
|
||||
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
|
||||
</t>
|
||||
<t t-if="i.state != 'paid'">
|
||||
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
|
||||
</t>
|
||||
|
||||
<div class="navspy" t-ignore="true" role="complementary">
|
||||
<ul class="nav flex-column bs-sidenav" data-id="quote_sidebar"></ul>
|
||||
</div>
|
||||
|
||||
<div t-if="not sale_order.is_expired and sale_order.state in ['draft', 'sent']" class="pl-3 pr-3">
|
||||
<input type="hidden" t-att-value="sale_order.validity_date" id="validity_date"/>
|
||||
<div class="mt8" t-if="sale_order.remaining_validity_days > 0">
|
||||
<hr class="mb0"/>
|
||||
<strong>This offer expires in</strong>
|
||||
<div class="day_counter mt8 mb16">
|
||||
<i class="fa fa-clock-o" aria-title="Dates" title="Dates"></i>
|
||||
<t t-esc="sale_order.remaining_validity_days"/>
|
||||
<t t-if="sale_order.remaining_validity_days > 1">
|
||||
days
|
||||
</t>
|
||||
<t t-if="sale_order.remaining_validity_days <= 1">
|
||||
day
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb16" t-if="sale_order.amount_undiscounted > sale_order.amount_untaxed">
|
||||
<hr class="mb0"/>
|
||||
<p class="text-muted mb8">Your advantage:</p>
|
||||
<t t-if="sale_order.amount_untaxed == sale_order.amount_total">
|
||||
<strong t-field="sale_order.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
<strong t-field="sale_order.amount_undiscounted"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
style="text-decoration: line-through"
|
||||
class="text-danger"/>
|
||||
</t>
|
||||
<t t-if="sale_order.amount_untaxed != sale_order.amount_total">
|
||||
<strong t-field="sale_order.amount_untaxed"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
<strong t-field="sale_order.amount_undiscounted"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
style="text-decoration: line-through"
|
||||
class="text-danger"/>
|
||||
<br />
|
||||
(<span t-field="sale_order.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/> Incl. tax)
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb8 ml16 mr16" t-if="sale_order.user_id">
|
||||
<hr class="mb0"/>
|
||||
<div><strong>Your Contact:</strong></div>
|
||||
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="sale_order.user_id.image" t-attf-src="data:image/png;base64,#{sale_order.user_id.image}" alt="Contact"/>
|
||||
<img class="rounded-circle mr4 float-left o_portal_contact_img" t-if="not sale_order.user_id.image" src="/web/static/src/img/placeholder.png" alt="Contact"/>
|
||||
<span t-field="sale_order.user_id" t-options='{"widget": "contact", "fields": ["name", "phone"], "no_marker": True}'/>
|
||||
<a href="#discussion"><i class="fa fa-comment"></i> Contact us</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<!-- Page content -->
|
||||
<div id="quote_content" class="col-lg-9">
|
||||
|
||||
<div class="alert alert-success alert-dismissable" t-if="message==1" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Your message has been successfully sent!
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==3" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
This order has been validated. Thanks for your trust
|
||||
and do not hesitate to <a href="#discussion">contact us</a> for
|
||||
any question.
|
||||
</div>
|
||||
<t t-if="sale_order.transaction_ids">
|
||||
<t t-call="payment.payment_confirmation_status">
|
||||
<t t-set="payment_tx_id" t-value="sale_order.get_portal_last_transaction()"/>
|
||||
</t>
|
||||
</t>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==4 and sale_order.state != 'sent'" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
This order
|
||||
<t t-if="sale_order.state=='draft'">has not yet been sent!</t>
|
||||
<t t-if="sale_order.state=='cancel'">has already been cancelled!</t>
|
||||
<t t-if="sale_order.state not in ('cancel','draft')">has already been validated!</t>
|
||||
You can <a href="#discussion">contact us</a> for
|
||||
any questions.
|
||||
</div>
|
||||
|
||||
<!-- modal relative to the actions Accept/Reject/Cancel -->
|
||||
<div role="dialog" class="modal fade" id="modalaccept">
|
||||
<div class="modal-dialog" t-if="not sale_order.signature and sale_order.has_to_be_signed()">
|
||||
<form id="accept" method="POST" t-att-data-order-id="sale_order.id" t-att-data-token="sale_order.access_token" class="js_accept_json modal-content js_website_submit_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Validate Order</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by signing this proposal, I accept it on the behalf of </span>
|
||||
<b t-field="sale_order.partner_id.commercial_partner_id"/>
|
||||
<span>, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="sale_order.payment_term_id"/>.
|
||||
</p>
|
||||
<t t-call="portal.portal_signature">
|
||||
<t t-set="object" t-value="sale_order"/>
|
||||
<t t-set="partner_name" t-value="sale_order.partner_id.name"/>
|
||||
<t t-set="callUrl" t-value="sale_order.get_portal_url('/accept')"/>
|
||||
<t t-set="accessToken" t-value="sale_order.access_token"/>
|
||||
</t>
|
||||
</main>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-dialog modal-content" t-if="not (sale_order.has_to_be_signed() and not sale_order.signature) and (sale_order.has_to_be_paid() or need_payment)">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Validate Order</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by paying this proposal, I accept it on the behalf of </span>
|
||||
<b t-field="sale_order.partner_id.commercial_partner_id"/><span>
|
||||
, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="sale_order.payment_term_id"/>.
|
||||
</p>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
|
||||
<h3 class="mb24">Pay with</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="submit_txt" t-value="'Pay & Confirm'"/>
|
||||
<t t-set="icon_class" t-value="'fa-lock'"/>
|
||||
<t t-set="form_action" t-value="sale_order.get_portal_url('/transaction/token')"/>
|
||||
<t t-set="prepare_tx_url" t-value="sale_order.get_portal_url('/transaction/')"/>
|
||||
<t t-set="access_token" t-value="sale_order.access_token"/>
|
||||
</t>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="dialog" class="modal fade" id="modaldecline">
|
||||
<div class="modal-dialog">
|
||||
<form id="decline" method="POST" t-attf-action="/my/orders/#{sale_order.id}/decline?access_token=#{sale_order.access_token}" class="modal-content">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Reject This Quotation</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<p>
|
||||
Tell us why you are refusing this quotation, this will help us improve our services.
|
||||
</p>
|
||||
<textarea rows="4" name="decline_message" placeholder="Your feedback....." class="form-control"/>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<button type="submit" t-att-id="sale_order.id" class="btn btn-primary">Reject</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger alert-dismissable d-print-none" t-if="sale_order.state == 'cancel'" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="close">&times;</button>
|
||||
<strong>This quotation has been canceled.</strong> Contact us to get a new quotation.
|
||||
</div>
|
||||
|
||||
<t t-call="sale.sale_order_portal_content"/>
|
||||
|
||||
<t t-if="sale_order.team_id.team_type != 'website'">
|
||||
|
||||
<div class="text-center d-print-none mt16 mb16" id="quotation_toolbar" t-if="sale_order.state in ('draft', 'sent', 'waiting_date')">
|
||||
<t t-set="show_buttons" t-value="False"/>
|
||||
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="not sale_order.is_expired and not sale_order.signature and sale_order.has_to_be_signed()" href="#">
|
||||
<i class="fa fa-check"></i> Accept
|
||||
<t t-set="show_buttons" t-value="True"/>
|
||||
</a>
|
||||
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-elif="not sale_order.is_expired and sale_order.has_to_be_paid()" href="#">
|
||||
<i class="fa fa-check"/> <t t-if="not sale_order.signature">Accept & </t>Pay
|
||||
<t t-set="show_buttons" t-value="True"/>
|
||||
</a>
|
||||
<a role="button" class="btn btn-info" type="submit" href="#discussion" t-if="show_buttons">
|
||||
<i class="fa fa-comment"></i> Feedback
|
||||
</a>
|
||||
<a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline" href="#" t-if="show_buttons">
|
||||
<i class="fa fa-times"></i> Reject
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center" t-if="need_payment or (sale_order.state in ('sale') and sale_order.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
|
||||
<a role="button" class="btn btn-primary" data-toggle="modal" data-target="#modalaccept" href="#">
|
||||
<i class="fa fa-check"></i> Pay Now
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div id="sale_order_communication">
|
||||
<h1 class="page-header">Communication</h1>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="sale_order"/>
|
||||
</t>
|
||||
</div>
|
||||
</div><!-- //col-lg-9 -->
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Sales Order content : intro, informations, order lines, remarks, descriptions ....
|
||||
This template should contains all the printable element of the SO. This is the
|
||||
template rendered in PDF with the report engine.
|
||||
-->
|
||||
<template id="sale_order_portal_content" name="Sales Order Portal Content">
|
||||
<!-- Intro -->
|
||||
<div id="introduction" class="page-header mt16">
|
||||
<h1>
|
||||
<t t-if="sale_order.state in ('draft','sent','cancel')">Quotation</t>
|
||||
<t t-if="sale_order.state not in ('draft','sent','cancel')">Sales Order</t>
|
||||
<em t-esc="sale_order.name"/>
|
||||
<div t-ignore="true" class="float-md-right css_editable_mode_hidden d-print-none">
|
||||
<ul class="list-inline">
|
||||
<li groups="sales_team.group_sale_salesman" class="list-inline-item">
|
||||
<a role="button" t-if="sale_order.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (sale_order._name, sale_order.id, action)">Edit Quotation</a>
|
||||
<a role="button" t-if="sale_order.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (sale_order._name, sale_order.id, action)">Back to Sales Order</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a role="button" class="btn btn-info" target="_blank" t-att-href="sale_order.get_portal_url() + '&pdf=True'">Print</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Informations -->
|
||||
<div id="informations">
|
||||
<div t-if="sale_order.transaction_ids and not invoices and sale_order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" t-att-data-order-id="sale_order.id">
|
||||
<t t-if="sale_order.transaction_ids">
|
||||
<t t-call="payment.payment_confirmation_status">
|
||||
<t t-set="payment_tx_id" t-value="sale_order.get_portal_last_transaction()"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="mb8">
|
||||
<strong>Date:</strong> <span t-field="sale_order.create_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div class="mb8" t-if="sale_order.validity_date">
|
||||
<strong>Expiration Date:</strong> <span t-field="sale_order.validity_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div class='row'>
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<strong>Invoicing Address</strong>
|
||||
</div>
|
||||
<div>
|
||||
<address t-field="sale_order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="shipping_address" class="col-lg-6" groups="sale.group_delivery_invoice_address">
|
||||
<div>
|
||||
<strong>Shipping Address</strong>
|
||||
</div>
|
||||
<div>
|
||||
<address t-field="order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
|
||||
<t t-set="display_discount" t-value="any([l.discount for l in order.order_line])"/>
|
||||
<div class="row">
|
||||
<div t-att-class="'col-xl-4 col-md-4 col-5' if display_discount else 'col-md-5 col-6'">
|
||||
<strong>Product</strong>
|
||||
</div>
|
||||
<div class="col-md-3 d-none d-md-block text-right">
|
||||
<strong>Unit Price</strong>
|
||||
</div>
|
||||
<div t-if="display_discount" class="col-1 text-right" >
|
||||
<strong>Discount</strong>
|
||||
</div>
|
||||
<div class="d-none d-md-block col-md-2 text-right">
|
||||
<strong>Quantity</strong>
|
||||
</div>
|
||||
<div class="col-2 d-md-none text-right">
|
||||
<strong>Qty.</strong>
|
||||
</div>
|
||||
<div class="col-md-2 col-4 text-right">
|
||||
<strong>Subtotal</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"/>
|
||||
|
||||
<div t-attf-class="row orders_vertical_align {{ 'o_is_' + line.display_type if line.display_type else '' }} pt8 pb8">
|
||||
<t t-if="not line.display_type">
|
||||
<div t-if="not request.env.user._is_public()" class="d-none d-xl-block col-xl-1 text-center">
|
||||
<span t-field="line.product_image" t-options="{'widget': 'image', 'class': 'rounded', 'style':'width: 48px;height:48px'}" />
|
||||
</div>
|
||||
<div id='product_name' t-att-class="'col-xl-3 col-md-4 col-5' if display_discount else 'col-xl-4 col-md-5 col-6'">
|
||||
<span t-field="line.product_id.name"/>
|
||||
</div>
|
||||
<div t-if="request.env.user._is_public()" class="d-none d-xl-block col-xl-1 text-center" /> <!-- Just for alignment -->
|
||||
<div class="col-md-3 d-none d-md-block text-right">
|
||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
|
||||
<div>
|
||||
<strong>Shipping Address</strong>
|
||||
</div>
|
||||
<div t-if="display_discount" class="col-1 text-right" style='white-space: nowrap'>
|
||||
<span t-field="line.discount"/> %
|
||||
<div>
|
||||
<address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
|
||||
</div>
|
||||
<div class="col-2 text-right">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
</div>
|
||||
<div class="col-md-2 col-4 text-right">
|
||||
<span
|
||||
t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<span
|
||||
t-field="line.price_total"
|
||||
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<div class="col-12">
|
||||
<span t-field="line.name"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="line.display_type == 'line_section'">
|
||||
<t t-set="current_section" t-value="line"/>
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
<div class="col-12">
|
||||
<span t-field="line.name"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<!--
|
||||
If we have a currently running subtotal, we need to display it
|
||||
when we find a new section or when we reach the end of the loop.
|
||||
-->
|
||||
<t t-if="current_section and (line_last or order.order_line[line_index+1].display_type == 'line_section')">
|
||||
<div t-attf-class="row orders_vertical_align is-subtotal pt8 pb8">
|
||||
<div class="col-12 text-right">
|
||||
<strong>Subtotal:</strong> <span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<strong>Contact</strong>
|
||||
</div>
|
||||
<div t-field="order.user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone"]}'/>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="row mb4">
|
||||
<div class="col-lg-9 col-8 text-right">
|
||||
<strong>Subtotal</strong>
|
||||
</div>
|
||||
<div class="col-lg-3 col-4 text-right">
|
||||
<strong>
|
||||
<span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb4">
|
||||
<t t-set="tax_groups" t-value="order.amount_by_group"/>
|
||||
<t t-foreach="tax_groups" t-as="amount_by_group">
|
||||
<t t-if="len(tax_groups) == 1 and order.amount_untaxed == amount_by_group[2]">
|
||||
<div class="col-lg-9 col-8 text-right"><span t-esc="amount_by_group[0]"/></div>
|
||||
<div class="col-lg-3 col-4 text-right">
|
||||
<span t-esc="amount_by_group[1]"
|
||||
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else ="">
|
||||
<div class="col-lg-9 col-8 text-right">
|
||||
<span t-esc="amount_by_group[0]"/>
|
||||
<span>&nbsp;<span>on</span>&nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></span>
|
||||
</div>
|
||||
<div class="col-lg-3 col-4 text-right">
|
||||
<span t-esc="amount_by_group[1]"
|
||||
t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="invoices" t-value="[i for i in sale_order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
|
||||
<t t-if="invoices">
|
||||
<div>
|
||||
<strong>Invoices</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-foreach="invoices" t-as="i">
|
||||
<t t-set="report_url" t-value="'/my/invoices/pdf/%s?%s' % (i.id, 'access_token=%s' % i.access_token if access_token else '')"/>
|
||||
<div>
|
||||
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
|
||||
<a t-att-href="report_url"><span t-esc="i.number"/></a>
|
||||
<span class="text-muted" t-field="i.date_invoice"/>
|
||||
<t t-if="i.state == 'paid'">
|
||||
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Paid</span>
|
||||
</t>
|
||||
<t t-if="i.state != 'paid'">
|
||||
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting Payment</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-8 text-right">
|
||||
<strong>Total</strong>
|
||||
</div>
|
||||
<div class="col-lg-3 col-4 text-right">
|
||||
<strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row" t-if="order.state == 'sent' and portal_confirmation == 'sign'" id="portal_sign">
|
||||
<div class="offset-lg-3 col-lg-6">
|
||||
<h4>Accept & Sign</h4>
|
||||
<t t-call="portal.portal_signature">
|
||||
<t t-set="object" t-value="order"/>
|
||||
<t t-set="partner_name" t-value="order.partner_id.name"/>
|
||||
<t t-set="callUrl" t-value="'/my/quotes/accept'"/>
|
||||
<t t-set="signLabel" t-value="'Accept Order'"/>
|
||||
<t t-set="accessToken" t-value="order.access_token"/>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
<!-- Pricing -->
|
||||
<!-- Signature -->
|
||||
<section id="details">
|
||||
<h1 class="page-header" id="details">Details</h1>
|
||||
|
||||
<t t-set="display_taxamount" t-value="any([line.tax_id for line in sale_order.order_line])"/>
|
||||
<t t-set="display_discount" t-value="True in [line.discount > 0 for line in sale_order.order_line]"/>
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="table" id="sales_order_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<t t-set="colspan" t-value="6"/>
|
||||
<th></th>
|
||||
<th>Products</th>
|
||||
<th>Quantity</th>
|
||||
<th t-if="display_taxamount">
|
||||
<span>Taxes</span>
|
||||
<t t-set="colspan" t-value="colspan+1"/>
|
||||
</th>
|
||||
<th t-if="display_discount">
|
||||
<span>Discount</span>
|
||||
<t t-set="colspan" t-value="colspan+1"/>
|
||||
</th>
|
||||
<th class="text-right">Unit Price</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="sale_tbody">
|
||||
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
|
||||
<t t-foreach="sale_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-attf-class="{{ 'o_is_' + line.display_type if line.display_type else '' }}">
|
||||
<t t-if="not line.display_type">
|
||||
<td class="text-center">
|
||||
<div class="d-none d-xl-block">
|
||||
<span t-field="line.product_image" t-options="{'widget': 'image', 'class': 'rounded', 'style':'width: 48px;height:48px'}" />
|
||||
</div>
|
||||
</td>
|
||||
<td id="product_name"><span t-field="line.name"/></td>
|
||||
<td>
|
||||
<div id="quote_qty">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
<span t-field="line.product_uom" groups="uom.group_uom"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="display_taxamount">
|
||||
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
|
||||
</td>
|
||||
<td t-if="display_discount">
|
||||
<strong t-if="line.discount" class="text-info">
|
||||
<t t-esc="((line.discount % 1) and '%s' or '%d') % line.discount"/>% discount
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div
|
||||
t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
t-att-style="line.discount and 'text-decoration: line-through' or None"
|
||||
t-att-class="(line.discount and 'text-danger' or '') + ' text-right'"
|
||||
/>
|
||||
<div t-if="line.discount">
|
||||
<t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<span
|
||||
t-field="line.price_total"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td t-att-colspan="colspan">
|
||||
<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 t-att-colspan="colspan">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-if="current_section and (line_last or sale_order.order_line[line_index+1].display_type == 'line_section')">
|
||||
<tr class="is-subtotal text-right">
|
||||
<td t-att-colspan="colspan">
|
||||
<strong>Subtotal:</strong> <span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<div id="total" class="row" name="total">
|
||||
<div class="col-8 col-sm-6 col-md-4 ml-auto">
|
||||
<table class="table table-sm">
|
||||
<tr class="border-black" style="border-bottom:1px solid #dddddd;">
|
||||
<td><strong>Subtotal</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
data-id="total_amount"
|
||||
t-field="sale_order.amount_untaxed"
|
||||
t-options='{"widget": "monetary","display_currency": sale_order.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="sale_order.amount_by_group" t-as="amount_by_group">
|
||||
<tr style="border-bottom:1px solid #dddddd;">
|
||||
<t t-if="amount_by_group[3] == 1 and sale_order.amount_untaxed == amount_by_group[2]">
|
||||
<td>
|
||||
<span t-esc="amount_by_group[0]"/>
|
||||
<span>&nbsp;<span>on</span>&nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="amount_by_group[1]"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.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": sale_order.pricelist_id.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="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure mb32"/>
|
||||
</t>
|
||||
</section>
|
||||
|
||||
<section id="terms" class="container" t-if="sale_order.note">
|
||||
<h2 class="page-header">Terms & Conditions</h2>
|
||||
<p t-field="sale_order.note"/>
|
||||
</section>
|
||||
|
||||
<section t-if="sale_order.signature" id="signature" name="Signature">
|
||||
<div class="row mt64" name="signature">
|
||||
<div class="offset-8 col-4">
|
||||
<strong>Signature</strong>
|
||||
</div>
|
||||
<div class="offset-8 col-4">
|
||||
<img t-att-src="'data:image/png;base64,%s' % to_text(sale_order.signature)" style="max-height: 4cm; max-width: 8cm;"/>
|
||||
</div>
|
||||
<div class="offset-8 col-4 text-center">
|
||||
<p t-field="sale_order.signed_by"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<template id="portal_order_error" name="Order error/warning display">
|
||||
<div class="row mr16">
|
||||
<div t-attf-class="'col-lg-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}" role="alert">
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
|
||||
<t t-if="error == 'generic'" name="generic">
|
||||
There was an error processing this page.
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_success" name="Order success display">
|
||||
<div class="row mr16">
|
||||
<div class="col-lg-12 mr16 ml16 alert alert-dismissable alert-success" role="status">
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -209,14 +209,22 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sales Order" class="o_sale_order">
|
||||
<header>
|
||||
<field name="authorized_transaction_ids" invisible="1"/>
|
||||
<button name="payment_action_capture" type="object"
|
||||
string="Capture Transaction" class="oe_highlight"
|
||||
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
|
||||
<button name="payment_action_void" type="object"
|
||||
string="Void Transaction"
|
||||
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
|
||||
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
|
||||
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary"/>
|
||||
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" class="btn-primary" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('invoice_count','>=',1)]}" context="{'proforma': True}"/>
|
||||
<button name="print_quotation" string="Print" type="object" states="draft" class="btn-primary o_sale_print"/>
|
||||
<button name="action_confirm" id="action_confirm"
|
||||
string="Confirm Sale" class="btn-primary" type="object"
|
||||
string="Confirm" class="btn-primary" type="object"
|
||||
attrs="{'invisible': [('state', 'not in', ['sent'])]}"/>
|
||||
<button name="action_confirm"
|
||||
string="Confirm Sale" type="object"
|
||||
string="Confirm" type="object"
|
||||
attrs="{'invisible': [('state', 'not in', ['draft'])]}"/>
|
||||
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','>=',1)]}" context="{'proforma': True}"/>
|
||||
<button name="print_quotation" string="Print" type="object" states="sent,sale" class="o_sale_print"/>
|
||||
|
||||
@@ -13,6 +13,8 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
|
||||
|
||||
cls.setUpExpenseProducts()
|
||||
|
||||
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
|
||||
|
||||
# partner and SO
|
||||
cls.partner_customer = cls.env['res.partner'].create({
|
||||
'name': 'Ze Client',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
from odoo.api import Environment, SUPERUSER_ID
|
||||
|
||||
|
||||
|
||||
@@ -41,12 +41,22 @@ The Dashboard for the Sales Manager will include
|
||||
* Monthly Turnover (Graph)
|
||||
""",
|
||||
'website': 'https://www.odoo.com/page/sales',
|
||||
'depends': ['sale', 'account', 'digest'],
|
||||
'depends': ['sale', 'digest'],
|
||||
'data': [
|
||||
'security/sale_management_security.xml',
|
||||
'data/sale_order_template_data.xml',
|
||||
'views/sale_portal_templates.xml',
|
||||
'views/sale_order_template_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml',
|
||||
'data/digest_data.xml',
|
||||
'views/sale_management_views.xml',
|
||||
'views/sale_management_templates.xml',
|
||||
'views/digest_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'report/sale_report_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/sale_order_template_demo.xml',
|
||||
],
|
||||
'application': True,
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
from . import portal
|
||||
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
from odoo.addons.portal.controllers.mail import _message_post_helper
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
@http.route(['/my/orders/<int:order_id>/accept'], type='json', auth="public", website=True)
|
||||
def portal_quote_accept(self, res_id, access_token=None, partner_name=None, signature=None, order_id=None):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', res_id, access_token=access_token)
|
||||
except AccessError:
|
||||
return {'error': _('Invalid order')}
|
||||
if order_sudo.state != 'sent':
|
||||
return {'error': _('Order is not in a state requiring customer validation.')}
|
||||
|
||||
if not order_sudo.has_to_be_signed() and not order_sudo.has_to_be_paid():
|
||||
return {'error': _('Operation not allowed')}
|
||||
if not signature:
|
||||
return {'error': _('Signature is missing.')}
|
||||
|
||||
success_message = _('Your order has been signed but still needs to be paid to be confirmed.')
|
||||
|
||||
if not order_sudo.has_to_be_paid():
|
||||
order_sudo.action_confirm()
|
||||
success_message = _('Your order has been confirmed.')
|
||||
|
||||
order_sudo.signature = signature
|
||||
order_sudo.signed_by = partner_name
|
||||
|
||||
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
|
||||
_message_post_helper(
|
||||
res_model='sale.order',
|
||||
res_id=order_sudo.id,
|
||||
message=_('Order signed by %s') % (partner_name,),
|
||||
attachments=[('%s.pdf' % order_sudo.name, pdf)],
|
||||
**({'token': access_token} if access_token else {}))
|
||||
|
||||
return {
|
||||
'success': success_message,
|
||||
'redirect_url': order_sudo.get_portal_url(),
|
||||
}
|
||||
|
||||
@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:
|
||||
return request.redirect('/my')
|
||||
|
||||
if order_sudo.state != 'sent':
|
||||
return request.redirect(order_sudo.get_portal_url() + "&message=4")
|
||||
order_sudo.action_cancel()
|
||||
message = post.get('decline_message')
|
||||
if message:
|
||||
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': access_token} if access_token else {})
|
||||
return request.redirect(order_sudo.get_portal_url())
|
||||
|
||||
@http.route(['/my/orders/<int:order_id>/update_line'], type='json', auth="public", website=True)
|
||||
def update(self, line_id, remove=False, unlink=False, order_id=None, access_token=None, **post):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except AccessError:
|
||||
return request.redirect('/my')
|
||||
|
||||
if order_sudo.state not in ('draft', 'sent'):
|
||||
return False
|
||||
order_line = request.env['sale.order.line'].sudo().browse(int(line_id))
|
||||
if order_line.order_id != order_sudo:
|
||||
return False
|
||||
if unlink:
|
||||
order_line.unlink()
|
||||
return False # return False to reload the page, the line must move back to options and the JS doesn't handle it
|
||||
number = -1 if remove else 1
|
||||
quantity = order_line.product_uom_qty + number
|
||||
order_line.write({'product_uom_qty': quantity})
|
||||
return [str(quantity), str(order_sudo.amount_total)]
|
||||
|
||||
@http.route(["/my/orders/<int:order_id>/add_option/<int:option_id>"], type='http', auth="public", website=True)
|
||||
def add(self, order_id, option_id, access_token=None, **post):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except AccessError:
|
||||
return request.redirect('/my')
|
||||
|
||||
option_sudo = request.env['sale.order.option'].sudo().browse(option_id)
|
||||
|
||||
if order_sudo != option_sudo.order_id:
|
||||
return request.redirect(order_sudo.get_portal_url())
|
||||
|
||||
option_sudo.add_option_to_order()
|
||||
|
||||
return request.redirect(option_sudo.order_id.get_portal_url() + "#details")
|
||||
|
||||
# note dbo: 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:
|
||||
return False
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'type': order._get_payment_type(),
|
||||
}
|
||||
|
||||
transaction = order._create_payment_transaction(vals)
|
||||
|
||||
return transaction.render_sale_button(
|
||||
order,
|
||||
order.get_portal_url(),
|
||||
submit_txt=_('Pay & Confirm'),
|
||||
render_values={
|
||||
'type': order._get_payment_type(),
|
||||
'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:
|
||||
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',
|
||||
}
|
||||
|
||||
order._create_payment_transaction(vals)
|
||||
|
||||
return request.redirect(order.get_portal_url())
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Mails & quote templates is done in a NOUPDATE block
|
||||
so users can freely customize/delete them -->
|
||||
<data noupdate="1">
|
||||
<record id="sale_order_template_default" model="sale.order.template">
|
||||
<field name="name">Default Template</field>
|
||||
<field name="number_of_days">30</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="website_quote_template_1" model="sale.quote.template">
|
||||
<record id="sale_order_template_1" model="sale.order.template">
|
||||
<field name="name">4 Person Desk</field>
|
||||
<field name="number_of_days">45</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_order_line_1" model="sale.quote.line">
|
||||
<field name="quote_id" ref="website_quote_template_1"/>
|
||||
<record id="sale_order_template_line_1" model="sale.order.template.line">
|
||||
<field name="sale_order_template_id" ref="sale_order_template_1"/>
|
||||
<field name="name">4 Person Desk</field>
|
||||
<field name="product_id" ref="product.consu_delivery_03"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
@@ -15,8 +15,8 @@
|
||||
<field name="discount">10.00</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_option_line_1" model="sale.quote.option">
|
||||
<field name="template_id" ref="website_quote_template_1"/>
|
||||
<record id="sale_order_template_option_1" model="sale.order.template.option">
|
||||
<field name="sale_order_template_id" ref="sale_order_template_1"/>
|
||||
<field name="name">Office Chair</field>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
<field name="quantity">4</field>
|
||||
@@ -1,3 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import digest
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
from . import sale_order
|
||||
from . import sale_order_template
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
group_sale_order_template = fields.Boolean("Quotation Templates", implied_group='sale_management.group_sale_order_template')
|
||||
default_sale_order_template_id = fields.Many2one('sale.order.template', default_model='sale.order', string='Default Template')
|
||||
module_sale_quotation_builder = fields.Boolean("Quotation Builder")
|
||||
|
||||
@api.onchange('group_sale_order_template')
|
||||
def _onchange_group_sale_order_template(self):
|
||||
if not self.group_sale_order_template:
|
||||
self.module_sale_quotation_builder = False
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
has_group_sale_order_template = fields.Boolean(
|
||||
"Quotation Templates",
|
||||
compute='_compute_groups_id', inverse='_inverse_groups_id',
|
||||
group_xml_id='sale_management.group_sale_order_template')
|
||||
@@ -4,86 +4,47 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.translate import html_translate
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
_description = "Sales Order Line"
|
||||
|
||||
website_description = fields.Html('Line Description', sanitize=False, translate=html_translate)
|
||||
option_line_id = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
|
||||
|
||||
# Take the description on the order template if the product is present in it
|
||||
@api.onchange('product_id')
|
||||
def product_id_change(self):
|
||||
domain = super(SaleOrderLine, self).product_id_change()
|
||||
if self.product_id and self.order_id.template_id:
|
||||
for quote_line in self.order_id.template_id.quote_line:
|
||||
if quote_line.product_id == self.product_id:
|
||||
self.name = quote_line.name
|
||||
break
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
values = self._inject_quote_description(values)
|
||||
return super(SaleOrderLine, self).create(values)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
values = self._inject_quote_description(values)
|
||||
return super(SaleOrderLine, self).write(values)
|
||||
|
||||
def _inject_quote_description(self, values):
|
||||
values = dict(values or {})
|
||||
if not values.get('website_description') and values.get('product_id'):
|
||||
product = self.env['product.product'].browse(values['product_id'])
|
||||
values['website_description'] = product.quote_description or product.website_description
|
||||
return values
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
def _get_default_template(self):
|
||||
template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
|
||||
return template and template.active and template or False
|
||||
def _get_default_sale_order_template(self):
|
||||
if not self.env.user.has_group('sale_management.group_sale_order_template'):
|
||||
return False
|
||||
template = self.env.ref('sale_management.sale_order_template_default', raise_if_not_found=False)
|
||||
return template if template and template.active else False
|
||||
|
||||
def _get_default_require_signature(self):
|
||||
default_template = self._get_default_template()
|
||||
default_template = self._get_default_sale_order_template()
|
||||
if default_template:
|
||||
return default_template.require_signature
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_default_require_payment(self):
|
||||
default_template = self._get_default_template()
|
||||
default_template = self._get_default_sale_order_template()
|
||||
if default_template:
|
||||
return default_template.require_payment
|
||||
else:
|
||||
return False
|
||||
|
||||
template_id = fields.Many2one(
|
||||
'sale.quote.template', 'Quotation Template',
|
||||
sale_order_template_id = fields.Many2one(
|
||||
'sale.order.template', 'Quotation Template',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
default=_get_default_template)
|
||||
website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate)
|
||||
options = fields.One2many(
|
||||
default=_get_default_sale_order_template)
|
||||
sale_order_option_ids = fields.One2many(
|
||||
'sale.order.option', 'order_id', 'Optional Products Lines',
|
||||
copy=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
|
||||
amount_undiscounted = fields.Float(
|
||||
'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
|
||||
quote_viewed = fields.Boolean('Quotation Viewed')
|
||||
require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature,
|
||||
require_signature = fields.Boolean('Online Signature', default=_get_default_require_signature,
|
||||
states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
|
||||
help='Request a digital signature to the customer in order to confirm orders automatically.')
|
||||
help='Request a online signature to the customer in order to confirm orders automatically.')
|
||||
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,
|
||||
states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
|
||||
help='Request an electronic payment to the customer in order to confirm orders automatically.')
|
||||
@@ -91,47 +52,48 @@ class SaleOrder(models.Model):
|
||||
@api.multi
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
if self.template_id and self.template_id.number_of_days > 0:
|
||||
if self.sale_order_template_id and self.sale_order_template_id.number_of_days > 0:
|
||||
default = dict(default or {})
|
||||
default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days))
|
||||
default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.sale_order_template_id.number_of_days))
|
||||
return super(SaleOrder, self).copy(default=default)
|
||||
|
||||
@api.one
|
||||
def _compute_amount_undiscounted(self):
|
||||
total = 0.0
|
||||
for line in self.order_line:
|
||||
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ??
|
||||
self.amount_undiscounted = total
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def onchange_partner_id(self):
|
||||
super(SaleOrder, self).onchange_partner_id()
|
||||
self.note = self.template_id.note or self.note
|
||||
self.note = self.sale_order_template_id.note or self.note
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def onchange_update_description_lang(self):
|
||||
if not self.template_id:
|
||||
return
|
||||
def _compute_line_data_for_template_change(self, line):
|
||||
return {
|
||||
'display_type': line.display_type,
|
||||
'name': line.name,
|
||||
'state': 'draft',
|
||||
}
|
||||
|
||||
def _compute_option_data_for_template_change(self, option):
|
||||
if self.pricelist_id:
|
||||
price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
|
||||
else:
|
||||
template = self.template_id.with_context(lang=self.partner_id.lang)
|
||||
self.website_description = template.website_description
|
||||
price = option.price_unit
|
||||
return {
|
||||
'product_id': option.product_id.id,
|
||||
'name': option.name,
|
||||
'quantity': option.quantity,
|
||||
'uom_id': option.uom_id.id,
|
||||
'price_unit': price,
|
||||
'discount': option.discount,
|
||||
}
|
||||
|
||||
@api.onchange('template_id')
|
||||
def onchange_template_id(self):
|
||||
if not self.template_id:
|
||||
@api.onchange('sale_order_template_id')
|
||||
def onchange_sale_order_template_id(self):
|
||||
if not self.sale_order_template_id:
|
||||
self.require_signature = False
|
||||
self.require_payment = False
|
||||
return
|
||||
template = self.template_id.with_context(lang=self.partner_id.lang)
|
||||
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
|
||||
|
||||
order_lines = [(5, 0, 0)]
|
||||
for line in template.quote_line:
|
||||
data = {
|
||||
'display_type': line.display_type,
|
||||
'name': line.name,
|
||||
'website_description': line.website_description,
|
||||
'state': 'draft',
|
||||
}
|
||||
for line in template.sale_order_template_line_ids:
|
||||
data = self._compute_line_data_for_template_change(line)
|
||||
if line.product_id:
|
||||
discount = 0
|
||||
if self.pricelist_id:
|
||||
@@ -145,7 +107,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
data.update({
|
||||
'price_unit': price,
|
||||
'discount': 100 - ((100 - discount) * (100 - line.discount)/100),
|
||||
'discount': 100 - ((100 - discount) * (100 - line.discount) / 100),
|
||||
'product_uom_qty': line.product_uom_qty,
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom': line.product_uom_id.id,
|
||||
@@ -159,47 +121,27 @@ class SaleOrder(models.Model):
|
||||
self.order_line._compute_tax_id()
|
||||
|
||||
option_lines = []
|
||||
for option in template.options:
|
||||
if self.pricelist_id:
|
||||
price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
|
||||
else:
|
||||
price = option.price_unit
|
||||
data = {
|
||||
'product_id': option.product_id.id,
|
||||
'name': option.name,
|
||||
'quantity': option.quantity,
|
||||
'uom_id': option.uom_id.id,
|
||||
'price_unit': price,
|
||||
'discount': option.discount,
|
||||
'website_description': option.website_description,
|
||||
}
|
||||
for option in template.sale_order_template_option_ids:
|
||||
data = self._compute_option_data_for_template_change(option)
|
||||
option_lines.append((0, 0, data))
|
||||
self.options = option_lines
|
||||
self.sale_order_option_ids = option_lines
|
||||
|
||||
if template.number_of_days > 0:
|
||||
self.validity_date = fields.Date.to_string(datetime.now() + timedelta(template.number_of_days))
|
||||
|
||||
self.website_description = template.website_description
|
||||
self.require_signature = template.require_signature
|
||||
self.require_payment = template.require_payment
|
||||
|
||||
if template.note:
|
||||
self.note = template.note
|
||||
|
||||
@api.constrains('template_id', 'require_signature', 'require_payment')
|
||||
def _check_portal_confirmation(self):
|
||||
for order in self.sudo().filtered('template_id'):
|
||||
if not order.require_signature and not order.require_payment:
|
||||
raise ValidationError(_('Please select a confirmation mode in Other Information: Digital Signature, Electronic Payment or both.'))
|
||||
|
||||
@api.multi
|
||||
def open_quotation(self):
|
||||
def preview_sale_order(self):
|
||||
self.ensure_one()
|
||||
self.write({'quote_viewed': True})
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'self',
|
||||
'url': '/quote/%s/%s' % (self.id, self.access_token)
|
||||
'url': self.get_portal_url(),
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@@ -208,11 +150,11 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
|
||||
|
||||
if not self.template_id or (not user.share and not self.env.context.get('force_website')):
|
||||
if not self.sale_order_template_id or (not user.share and not self.env.context.get('force_website')):
|
||||
return super(SaleOrder, self).get_access_action(access_uid)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/quote/%s/%s' % (self.id, self.access_token),
|
||||
'url': self.get_portal_url(),
|
||||
'target': 'self',
|
||||
'res_id': self.id,
|
||||
}
|
||||
@@ -221,12 +163,12 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
if self.state not in ['sale', 'done']:
|
||||
auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
|
||||
return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param
|
||||
return self.get_portal_url() + '&%s' % auth_param
|
||||
return super(SaleOrder, self)._get_share_url(redirect)
|
||||
|
||||
def get_portal_confirmation_action(self):
|
||||
""" Template override default behavior of pay / sign chosen in sales settings """
|
||||
if self.template_id:
|
||||
if self.sale_order_template_id:
|
||||
if self.require_signature and not self.signature:
|
||||
return 'sign'
|
||||
elif self.require_payment:
|
||||
@@ -237,18 +179,18 @@ class SaleOrder(models.Model):
|
||||
|
||||
def has_to_be_signed(self):
|
||||
res = super(SaleOrder, self).has_to_be_signed()
|
||||
return self.require_signature if self.template_id else res
|
||||
return self.require_signature if self.sale_order_template_id else res
|
||||
|
||||
def has_to_be_paid(self):
|
||||
res = super(SaleOrder, self).has_to_be_paid()
|
||||
return self.require_payment if self.template_id else res
|
||||
return self.require_payment if self.sale_order_template_id else res
|
||||
|
||||
@api.multi
|
||||
def action_confirm(self):
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
for order in self:
|
||||
if order.template_id and order.template_id.mail_template_id:
|
||||
self.template_id.mail_template_id.send_mail(order.id)
|
||||
if order.sale_order_template_id and order.sale_order_template_id.mail_template_id:
|
||||
self.sale_order_template_id.mail_template_id.send_mail(order.id)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
@@ -257,6 +199,24 @@ class SaleOrder(models.Model):
|
||||
return 'form_save' if self.require_payment else 'form'
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
_description = "Sales Order Line"
|
||||
|
||||
sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
|
||||
|
||||
# Take the description on the order template if the product is present in it
|
||||
@api.onchange('product_id')
|
||||
def product_id_change(self):
|
||||
domain = super(SaleOrderLine, self).product_id_change()
|
||||
if self.product_id and self.order_id.sale_order_template_id:
|
||||
for line in self.order_id.sale_order_template_id.sale_order_template_line_ids:
|
||||
if line.product_id == self.product_id:
|
||||
self.name = line.name
|
||||
break
|
||||
return domain
|
||||
|
||||
|
||||
class SaleOrderOption(models.Model):
|
||||
_name = "sale.order.option"
|
||||
_description = "Sale Options"
|
||||
@@ -266,7 +226,6 @@ class SaleOrderOption(models.Model):
|
||||
line_id = fields.Many2one('sale.order.line', on_delete="set null")
|
||||
name = fields.Text('Description', required=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
|
||||
website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate)
|
||||
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
||||
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
|
||||
uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True)
|
||||
@@ -279,7 +238,6 @@ class SaleOrderOption(models.Model):
|
||||
return
|
||||
product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
|
||||
self.price_unit = product.list_price
|
||||
self.website_description = product.quote_description or product.website_description
|
||||
self.name = product.name
|
||||
if product.description_sale:
|
||||
self.name += '\n' + product.description_sale
|
||||
@@ -293,28 +251,33 @@ class SaleOrderOption(models.Model):
|
||||
|
||||
@api.multi
|
||||
def button_add_to_order(self):
|
||||
self.ensure_one()
|
||||
order = self.order_id
|
||||
if order.state not in ['draft', 'sent']:
|
||||
return False
|
||||
self.add_option_to_order()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
|
||||
if order_line:
|
||||
order_line = order_line[0]
|
||||
order_line.product_uom_qty += 1
|
||||
else:
|
||||
vals = {
|
||||
'price_unit': self.price_unit,
|
||||
'website_description': self.website_description,
|
||||
'name': self.name,
|
||||
'order_id': order.id,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': self.quantity,
|
||||
'product_uom': self.uom_id.id,
|
||||
'discount': self.discount,
|
||||
}
|
||||
order_line = self.env['sale.order.line'].create(vals)
|
||||
order_line._compute_tax_id()
|
||||
@api.multi
|
||||
def add_option_to_order(self):
|
||||
self.ensure_one()
|
||||
|
||||
sale_order = self.order_id
|
||||
|
||||
if sale_order.state not in ['draft', 'sent']:
|
||||
raise UserError(_('You cannot add options to a confirmed order.'))
|
||||
|
||||
values = self._get_values_to_add_to_order()
|
||||
order_line = self.env['sale.order.line'].create(values)
|
||||
order_line._compute_tax_id()
|
||||
|
||||
self.write({'line_id': order_line.id})
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
@api.multi
|
||||
def _get_values_to_add_to_order(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'order_id': self.order_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': self.quantity,
|
||||
'product_uom': self.uom_id.id,
|
||||
'discount': self.discount,
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.translate import html_translate
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class SaleQuoteTemplate(models.Model):
|
||||
_name = "sale.quote.template"
|
||||
_description = "Sale Quotation Template"
|
||||
|
||||
class SaleOrderTemplate(models.Model):
|
||||
_name = "sale.order.template"
|
||||
_description = "Quotation Template"
|
||||
|
||||
def _get_default_require_signature(self):
|
||||
# A confirmation mode (sign or pay) is mandatory on a quotation template
|
||||
@@ -21,49 +21,31 @@ class SaleQuoteTemplate(models.Model):
|
||||
return self.env.user.company_id.portal_confirmation_pay
|
||||
|
||||
name = fields.Char('Quotation Template', required=True)
|
||||
website_description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
|
||||
quote_line = fields.One2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True)
|
||||
sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True)
|
||||
note = fields.Text('Terms and conditions')
|
||||
options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True)
|
||||
sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Lines', copy=True)
|
||||
number_of_days = fields.Integer('Quotation Duration',
|
||||
help='Number of days for the validity date computation of the quotation')
|
||||
require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature, help='Request a digital signature to the customer in order to confirm orders automatically.')
|
||||
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,help='Request an electronic payment to the customer in order to confirm orders automatically.')
|
||||
require_signature = fields.Boolean('Online Signature', default=_get_default_require_signature, help='Request a online signature to the customer in order to confirm orders automatically.')
|
||||
require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment, help='Request an electronic payment to the customer in order to confirm orders automatically.')
|
||||
mail_template_id = fields.Many2one(
|
||||
'mail.template', 'Confirmation Mail',
|
||||
domain=[('model', '=', 'sale.order')],
|
||||
help="This e-mail template will be sent on confirmation. Leave empty to send nothing.")
|
||||
active = fields.Boolean(default=True, help="If unchecked, it will allow you to hide the quotation template without removing it.")
|
||||
|
||||
@api.constrains('require_signature', 'require_payment')
|
||||
def _check_confirmation(self):
|
||||
for template in self:
|
||||
if not template.require_signature and not template.require_payment:
|
||||
raise ValidationError(_('Please select a confirmation mode in Confirmation: Digital Signature, Electronic Payment or both.'))
|
||||
|
||||
@api.multi
|
||||
def open_template(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'self',
|
||||
'url': '/quote/template/%d' % self.id
|
||||
}
|
||||
|
||||
|
||||
class SaleQuoteLine(models.Model):
|
||||
_name = "sale.quote.line"
|
||||
_description = "Quotation Template Lines"
|
||||
_order = 'quote_id, sequence, id'
|
||||
class SaleOrderTemplateLine(models.Model):
|
||||
_name = "sale.order.template.line"
|
||||
_description = "Quotation Template Line"
|
||||
_order = 'sale_order_template_id, sequence, id'
|
||||
|
||||
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of sale quote lines.",
|
||||
default=10)
|
||||
quote_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', required=True,
|
||||
sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', required=True,
|
||||
ondelete='cascade', index=True)
|
||||
name = fields.Text('Description', required=True, translate=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
|
||||
website_description = fields.Html('Line Description', related='product_id.product_tmpl_id.quote_description',
|
||||
translate=html_translate)
|
||||
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
||||
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
|
||||
product_uom_qty = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1)
|
||||
@@ -83,7 +65,6 @@ class SaleQuoteLine(models.Model):
|
||||
self.name = name
|
||||
self.price_unit = self.product_id.lst_price
|
||||
self.product_uom_id = self.product_id.uom_id.id
|
||||
self.website_description = self.product_id.quote_description or self.product_id.website_description or ''
|
||||
domain = {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
return {'domain': domain}
|
||||
|
||||
@@ -96,22 +77,13 @@ class SaleQuoteLine(models.Model):
|
||||
def create(self, values):
|
||||
if values.get('display_type', self.default_get(['display_type'])['display_type']):
|
||||
values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom_id=False)
|
||||
values = self._inject_quote_description(values)
|
||||
return super(SaleQuoteLine, self).create(values)
|
||||
return super(SaleOrderTemplateLine, self).create(values)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
||||
raise UserError("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.")
|
||||
values = self._inject_quote_description(values)
|
||||
return super(SaleQuoteLine, self).write(values)
|
||||
|
||||
def _inject_quote_description(self, values):
|
||||
values = dict(values or {})
|
||||
if not values.get('website_description') and values.get('product_id'):
|
||||
product = self.env['product.product'].browse(values['product_id'])
|
||||
values['website_description'] = product.quote_description or product.website_description or ''
|
||||
return values
|
||||
return super(SaleOrderTemplateLine, self).write(values)
|
||||
|
||||
_sql_constraints = [
|
||||
('accountable_product_id_required',
|
||||
@@ -124,15 +96,14 @@ class SaleQuoteLine(models.Model):
|
||||
]
|
||||
|
||||
|
||||
class SaleQuoteOption(models.Model):
|
||||
_name = "sale.quote.option"
|
||||
_description = "Quotation Option"
|
||||
class SaleOrderTemplateOption(models.Model):
|
||||
_name = "sale.order.template.option"
|
||||
_description = "Quotation Template Option"
|
||||
|
||||
template_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade',
|
||||
sale_order_template_id = fields.Many2one('sale.order.template', 'Quotation Template Reference', ondelete='cascade',
|
||||
index=True, required=True)
|
||||
name = fields.Text('Description', required=True, translate=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True)
|
||||
website_description = fields.Html('Option Description', translate=html_translate, sanitize_attributes=False)
|
||||
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
||||
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
|
||||
uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True)
|
||||
@@ -144,7 +115,6 @@ class SaleQuoteOption(models.Model):
|
||||
return
|
||||
product = self.product_id
|
||||
self.price_unit = product.list_price
|
||||
self.website_description = product.product_tmpl_id.quote_description
|
||||
self.name = product.name
|
||||
self.uom_id = product.uom_id
|
||||
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="report_saleorder_validity_date" inherit_id="sale.report_saleorder_document">
|
||||
<xpath expr="//div[hasclass('page')]/p[@id='fiscal_position_remark']" position="after">
|
||||
<div t-if="doc.options and doc.state in ['draft', 'sent']" style="page-break-before:always;">
|
||||
<div t-if="doc.sale_order_option_ids and doc.state in ['draft', 'sent']" style="page-break-before:always;">
|
||||
<h2>
|
||||
<span>Suggested Products</span>
|
||||
</h2>
|
||||
@@ -16,7 +16,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<tr t-foreach="doc.options" t-as="option">
|
||||
<tr t-foreach="doc.sale_order_option_ids" t-as="option">
|
||||
<td>
|
||||
<span t-field="option.product_id.name"/>
|
||||
</td>
|
||||
@@ -0,0 +1,9 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_order_template,sale.order.template,model_sale_order_template,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_order_template_manager,sale.order.template,model_sale_order_template,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_order_template_line,sale.order.template.line,model_sale_order_template_line,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_order_template_line_manager,sale.order.template.line,model_sale_order_template_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_order_template_option,sale.order.template.option,model_sale_order_template_option,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_order_template_option_manager,sale.order.template.option,model_sale_order_template_option,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_order_option,sale.order.option,model_sale_order_option,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_order_option_all,sale.order.option,model_sale_order_option,,1,0,0,0
|
||||
|
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="group_sale_order_template" model="res.groups">
|
||||
<field name="name">Quotation Templates</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,51 @@
|
||||
odoo.define('sale_management.sale_management', function (require) {
|
||||
'use strict';
|
||||
|
||||
require('web.dom_ready');
|
||||
var ajax = require('web.ajax');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
if (!$('.o_portal_sale_sidebar').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.o_portal_sale_sidebar'");
|
||||
}
|
||||
|
||||
// Add to SO button
|
||||
var UpdateLineButton = Widget.extend({
|
||||
events: {
|
||||
'click' : 'onClick',
|
||||
},
|
||||
onClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var href = this.$el.attr("href");
|
||||
var order_id = href.match(/my\/orders\/([0-9]+)/);
|
||||
var line_id = href.match(/update_line\/([0-9]+)/);
|
||||
var params = {
|
||||
'line_id': line_id[1],
|
||||
'remove': self.$el.is('[href*="remove"]'),
|
||||
'unlink': self.$el.is('[href*="unlink"]'),
|
||||
};
|
||||
var token = href.match(/token=(.*)/);
|
||||
if (token) {
|
||||
params.access_token = token;
|
||||
}
|
||||
var url = "/my/orders/" + parseInt(order_id[1]) + "/update_line";
|
||||
ajax.jsonRpc(url, 'call', params).then(function (data) {
|
||||
if (!data) {
|
||||
window.location.reload();
|
||||
}
|
||||
self.$el.parents('.input-group:first').find('.js_quantity').val(data[0]);
|
||||
$('[data-id="total_amount"]>span').html(data[1]);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
var update_button_list = [];
|
||||
$('a.js_update_line_json').each(function (index) {
|
||||
var button = new UpdateLineButton();
|
||||
button.setElement($(this)).start();
|
||||
update_button_list.push(button);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.sale.management</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='sale_config_online_confirmation_pay']" position="after">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_sale_order_template"/>
|
||||
<field name="module_sale_quotation_builder" invisible="1"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_sale_order_template"/>
|
||||
<div class="text-muted">
|
||||
Create standardized offers
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('group_sale_order_template', '=', False)]}">
|
||||
<div class="mt16">
|
||||
<label for="default_sale_order_template_id" class="o_light_label"/>
|
||||
<field name="default_sale_order_template_id" class="oe_inline"/>
|
||||
</div>
|
||||
<div>
|
||||
<button name="%(sale_management.sale_order_template_action)d" icon="fa-arrow-right" type="action" string="Quotation Templates" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': [('group_sale_order_template','=',False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_sale_quotation_builder"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_sale_quotation_builder"/>
|
||||
<div class="text-muted">
|
||||
Design your quotation templates using building blocks<br/>
|
||||
<!-- TODO SEB don't show this after the module is installed -->
|
||||
<em attrs="{'invisible': [('module_sale_quotation_builder','=',False)]}">Warning: this option will install the Website app.</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- inherits from sales_team.assets_backend to override certain attributes defined in sales_team -->
|
||||
<template id="assets_backend" name="project assets" inherit_id="sales_team.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/sale_management/static/src/js/tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sale_order_template_view_search" model="ir.ui.view">
|
||||
<field name="name">sale.order.template.search</field>
|
||||
<field name="model">sale.order.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Quotation Template">
|
||||
<filter string="Archived" name="inactive" domain="[('active','=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.template.form</field>
|
||||
<field name="model">sale.order.template</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Quotation Template">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Quotation Template"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<label for="number_of_days" string="Quotation expires after"/>
|
||||
<div id="number_of_days">
|
||||
<field name="number_of_days" class="oe_inline"/> days
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook name="main_book">
|
||||
<page string="Lines">
|
||||
<field name="sale_order_template_line_ids" widget="section_and_note_one2many">
|
||||
<form string="Quotation Template Lines">
|
||||
<!--
|
||||
We need the sequence field to be here for new lines to be added at the correct position.
|
||||
TODO: at some point we want to fix this in the framework so that an invisible field is not required.
|
||||
-->
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="display_type" invisible="1"/>
|
||||
<group attrs="{'invisible': [('display_type', '!=', False)]}">
|
||||
<group>
|
||||
<field
|
||||
name="product_id"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)]
|
||||
}"
|
||||
/>
|
||||
<label for="product_uom_qty"/>
|
||||
<div>
|
||||
<field
|
||||
name="product_uom_qty" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<label for="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<div groups="sale.group_discount_per_so_line">
|
||||
<field name="discount" class="oe_inline"/> %%
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook colspan="4" name="description">
|
||||
<page string="Description" attrs="{'invisible': [('display_type', '!=', False)]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
<page string="Section" attrs="{'invisible': [('display_type', '!=', 'line_section')]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
<page string="Note" attrs="{'invisible': [('display_type', '!=', 'line_note')]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
<tree string="Quotation Template Lines" editable="bottom">
|
||||
<control>
|
||||
<create string="Add a product"/>
|
||||
<create string="Add a section" context="{'default_display_type': 'line_section'}"/>
|
||||
<create string="Add a note" context="{'default_display_type': 'line_note'}"/>
|
||||
</control>
|
||||
|
||||
<field name="display_type" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name" widget="section_and_note_text"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field
|
||||
name="product_uom_id"
|
||||
groups="uom.group_uom"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)]
|
||||
}"
|
||||
/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Optional Products">
|
||||
<field name="sale_order_template_option_ids">
|
||||
<tree string="Quotation Template Lines" editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="uom.group_uom"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Confirmation">
|
||||
<group>
|
||||
<field name="require_signature"/>
|
||||
<field name="require_payment"/>
|
||||
<field name="mail_template_id"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="note" nolabel="1"
|
||||
placeholder="The Administrator can set default Terms & Conditions in Sales Settings. Terms set here will show up instead if you select this quotation template."/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sale.order.template.tree</field>
|
||||
<field name="model">sale.order.template</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Quotation Template">
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Quotation Templates</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.order.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your quotation template
|
||||
</p><p>
|
||||
Use templates to create polished, professional quotes in minutes.
|
||||
Send these quotes by email and let your customers sign online.
|
||||
Use cross-selling and discounts to push and boost your sales.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="sale_order_template_menu" action="sale_order_template_action" parent="sale.menu_sales_config" sequence="1" name="Quotation Templates" groups="sale_management.group_sale_order_template"/>
|
||||
|
||||
</odoo>
|
||||
@@ -6,14 +6,11 @@
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header/button[@name='action_confirm'][1]" position="after">
|
||||
<button name="open_quotation" type="object"
|
||||
class="o_open_quotation" string="Preview"
|
||||
attrs="{'invisible': [('template_id', '=', False)]}"/>
|
||||
<button name="preview_sale_order" type="object" string="Preview"/>
|
||||
</xpath>
|
||||
<xpath expr="//page/field[@name='order_line']/.." position="after">
|
||||
<page string="Suggested Products">
|
||||
<field name="quote_viewed" invisible="1"/>
|
||||
<field name="options">
|
||||
<page string="Optional Products">
|
||||
<field name="sale_order_option_ids">
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id"/>
|
||||
@@ -23,22 +20,19 @@
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<button name="button_add_to_order" class="oe_link" icon="fa-shopping-cart" string="Add to order lines" type="object"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_shipping_id']" position="after">
|
||||
<label for="template_id"/>
|
||||
<div>
|
||||
<field name="template_id" context="{'company_id': company_id}"
|
||||
options="{'no_create': True, 'no_open': True}"/>
|
||||
</div>
|
||||
<field name="sale_order_template_id" context="{'company_id': company_id}"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
groups="sale_management.group_sale_order_template"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='client_order_ref']" position="after">
|
||||
<field name="require_signature" attrs="{'invisible': [('template_id', '=', False)]}"/>
|
||||
<field name="require_payment" attrs="{'invisible': [('template_id', '=', False)]}"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
<field name="require_signature" attrs="{'invisible': [('sale_order_template_id', '=', False)]}"/>
|
||||
<field name="require_payment" attrs="{'invisible': [('sale_order_template_id', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Website Quote frontend assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/sale_management/static/src/js/sale_management.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="sale_order_portal_content_inherit_sale_management" name="Order Options" inherit_id="sale.sale_order_portal_content">
|
||||
|
||||
<xpath expr="//section[@id='details']//t[@t-set='display_discount']" position="after">
|
||||
<t t-set="display_remove" t-value="any([line.sale_order_option_ids for line in sale_order.order_line])"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//section[@id='details']//table[@id='sales_order_table']/thead/tr" position="inside">
|
||||
<th t-if="display_remove">
|
||||
<t t-set="colspan" t-value="colspan+1"/>
|
||||
</th>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//section[@id='details']//t[@t-if='not line.display_type']" position="inside">
|
||||
<td class="text-center" t-if="display_remove">
|
||||
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id) + '&unlink=True'" class="mb8 js_update_line_json d-print-none" t-if="line.sale_order_option_ids" aria-label="Remove" title="Remove">
|
||||
<span class="fa fa-trash-o"></span>
|
||||
</a>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//section[@id='signature']" position="after">
|
||||
<t t-if="any([(not option.line_id) for option in sale_order.sale_order_option_ids])">
|
||||
<section>
|
||||
<h1 class="page-header">Optional Products</h1>
|
||||
</section>
|
||||
<section>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th></th>
|
||||
<th class="text-right">Price</th>
|
||||
<th t-if="sale_order.state in ['draft', 'sent']"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="sale_order.sale_order_option_ids" t-as="option">
|
||||
<t t-if="not option.line_id">
|
||||
<td>
|
||||
<div t-field="option.name"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-if="option.discount" class="text-info">
|
||||
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong class="text-right">
|
||||
<div t-field="option.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
|
||||
t-att-style="option.discount and 'text-decoration: line-through' or None"
|
||||
t-att-class="option.discount and 'text-danger' or None"/>
|
||||
<div t-if="option.discount">
|
||||
<t t-esc="(1-option.discount / 100.0) * option.price_unit" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-center" t-if="sale_order.state in ['draft', 'sent']">
|
||||
<a t-att-href="sale_order.get_portal_url('/add_option/%s' % option.id)" class="mb8 d-print-none" aria-label="Add to cart" title="Add to cart">
|
||||
<span class="fa fa-shopping-cart"/>
|
||||
</a>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//section[@id='details']//div[@id='quote_qty']" position="replace">
|
||||
<t t-if="sale_order.state in ['draft', 'sent'] and line.sale_order_option_ids">
|
||||
<div class="input-group oe_website_spinner">
|
||||
|
||||
<div class="input-group-prepend d-print-none">
|
||||
<span class="input-group-text">
|
||||
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id) + '&remove=True'" class="mb8 js_update_line_json" aria-label="Remove one" title="Remove one">
|
||||
<span class="fa fa-minus"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- TODO add uom in this case too -->
|
||||
<input type="text" class="js_quantity form-control" t-att-data-id="line.id" t-att-value="line.product_uom_qty"/>
|
||||
<div class="input-group-append d-print-none">
|
||||
<span class="input-group-text">
|
||||
<a t-att-href="sale_order.get_portal_url('/update_line/%s' % line.id)" class="mb8 js_update_line_json" aria-label="Add one" title="Add one">
|
||||
<span class="fa fa-plus"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
<span t-field="line.product_uom" groups="uom.group_uom"/>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1,5 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Payment - Sale',
|
||||
'category': 'Sales',
|
||||
'summary': 'Link Sales and Payment',
|
||||
'version': '1.0',
|
||||
'description': """Link Sales and Payment
|
||||
|
||||
Provide tools for sale-related payment
|
||||
|
||||
* specific management of transactions like sale order confirmation
|
||||
* tools method to handle transactions when allowing to pay / sell
|
||||
* JS code to handle a payment form (eCommerce
|
||||
* add payment-related fields on SO
|
||||
""",
|
||||
'depends': ['payment', 'sale'],
|
||||
'data': [
|
||||
'views/sale_portal_templates.xml',
|
||||
'views/settings.xml',
|
||||
'views/payment_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
||||
from odoo.http import request, route
|
||||
|
||||
|
||||
class PaymentPortal(http.Controller):
|
||||
|
||||
@route('/pay/sale/<int:order_id>/form_tx', type='json', auth="public", website=True)
|
||||
def sale_pay_form(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 on the payment
|
||||
form.
|
||||
|
||||
:return html: form containing all values related to the acquirer to
|
||||
redirect customers to the acquirer website """
|
||||
success_url = '/my/orders/%s?access_token=%s' % (order_id, access_token) if access_token else '/my/orders/%s' % order_id
|
||||
success_url = kwargs.get('success_url', success_url)
|
||||
|
||||
order_sudo = request.env['sale.order'].sudo().browse(order_id)
|
||||
if not order_sudo:
|
||||
return False
|
||||
|
||||
try:
|
||||
acquirer_id = int(acquirer_id)
|
||||
except:
|
||||
return False
|
||||
|
||||
if request.env.user == request.env.ref('base.public_user'):
|
||||
save_token = False
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
}
|
||||
|
||||
if save_token:
|
||||
vals['type'] = 'form_save'
|
||||
|
||||
transaction = order_sudo._create_payment_transaction(vals)
|
||||
|
||||
return transaction.render_sale_button(
|
||||
order_sudo,
|
||||
success_url,
|
||||
submit_txt=_('Pay'),
|
||||
render_values={
|
||||
'type': 'form_save' if save_token else 'form',
|
||||
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
|
||||
}
|
||||
)
|
||||
|
||||
@http.route('/pay/sale/<int:order_id>/s2s_token_tx', type='http', auth='public', website=True)
|
||||
def sale_pay_token(self, order_id, pm_id=None, **kwargs):
|
||||
""" Use a token to perform a s2s transaction """
|
||||
error_url = kwargs.get('error_url', '/my')
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
access_token = kwargs.get('access_token')
|
||||
params = {}
|
||||
if access_token:
|
||||
params['access_token'] = access_token
|
||||
|
||||
order_sudo = request.env['sale.order'].sudo().browse(order_id).exists()
|
||||
if not order_sudo:
|
||||
params['error'] = 'pay_sale_invalid_doc'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
try:
|
||||
token = request.env['payment.token'].sudo().browse(int(pm_id))
|
||||
except (ValueError, TypeError):
|
||||
token = False
|
||||
|
||||
token_owner = order_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id
|
||||
if not token or token.partner_id != token_owner:
|
||||
params['error'] = 'pay_sale_invalid_token'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'payment_token_id': token.id,
|
||||
'type': 'server2server',
|
||||
}
|
||||
|
||||
order_sudo._create_payment_transaction(vals)
|
||||
|
||||
params['success'] = 'pay_sale'
|
||||
|
||||
return request.redirect(_build_url_w_params(success_url, params))
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
def _order_get_page_view_values(self, order, access_token, **kwargs):
|
||||
values = super(CustomerPortal, self)._order_get_page_view_values(order, access_token, **kwargs)
|
||||
if values['portal_confirmation'] == 'pay':
|
||||
payment_inputs = request.env['payment.acquirer']._get_available_payment_input(order.partner_id, order.company_id)
|
||||
# if not connected (using public user), the method _get_available_payment_input will return public user tokens
|
||||
is_public_user = request.env.ref('base.public_user') == request.env.user
|
||||
if is_public_user:
|
||||
# we should not display payment tokens owned by the public user
|
||||
payment_inputs.pop('pms', None)
|
||||
token_count = request.env['payment.token'].sudo().search_count([('acquirer_id.company_id', '=', order.company_id.id),
|
||||
('partner_id', '=', order.partner_id.id),
|
||||
])
|
||||
values['existing_token'] = token_count > 0
|
||||
values.update(payment_inputs)
|
||||
values['partner_id'] = order.partner_id if is_public_user else request.env.user.partner_id
|
||||
return values
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
automatic_invoice = fields.Boolean("Automatic Invoice",
|
||||
help="The invoice is generated automatically and available in the customer portal "
|
||||
"when the transaction is confirmed by the payment acquirer.\n"
|
||||
"The invoice is marked as paid and the payment is registered in the payment journal "
|
||||
"defined in the configuration of the payment acquirer.\n"
|
||||
"This mode is advised if you issue the final invoice at the order and not after the delivery.",
|
||||
config_parameter='sale_payment.automatic_invoice')
|
||||
template_id = fields.Many2one('mail.template', 'Email Template',
|
||||
domain="[('model', '=', 'account.invoice')]",
|
||||
config_parameter='sale_payment.default_email_template',
|
||||
default=lambda self: self.env.ref('account.email_template_edi_invoice', False))
|
||||
@@ -1,113 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
transaction_ids = fields.Many2many('payment.transaction', 'sale_order_transaction_rel', 'sale_order_id', 'transaction_id',
|
||||
string='Transactions', copy=False, readonly=True)
|
||||
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
|
||||
string='Authorized Transactions', copy=False, readonly=True)
|
||||
|
||||
@api.depends('transaction_ids')
|
||||
def _compute_authorized_transaction_ids(self):
|
||||
for trans in self:
|
||||
trans.authorized_transaction_ids = trans.transaction_ids.filtered(lambda t: t.state == 'authorized')
|
||||
|
||||
@api.multi
|
||||
def get_portal_last_transaction(self):
|
||||
self.ensure_one()
|
||||
return self.transaction_ids.get_last_transaction()
|
||||
|
||||
@api.multi
|
||||
def _create_payment_transaction(self, vals):
|
||||
'''Similar to self.env['payment.transaction'].create(vals) but the values are filled with the
|
||||
current sales orders fields (e.g. the partner or the currency).
|
||||
:param vals: The values to create a new payment.transaction.
|
||||
:return: The newly created payment.transaction record.
|
||||
'''
|
||||
# Ensure the currencies are the same.
|
||||
currency = self[0].pricelist_id.currency_id
|
||||
if any([so.pricelist_id.currency_id != currency for so in self]):
|
||||
raise ValidationError(_('A transaction can\'t be linked to sales orders having different currencies.'))
|
||||
|
||||
# Ensure the partner are the same.
|
||||
partner = self[0].partner_id
|
||||
if any([so.partner_id != partner for so in self]):
|
||||
raise ValidationError(_('A transaction can\'t be linked to sales orders having different partners.'))
|
||||
|
||||
# Try to retrieve the acquirer. However, fallback to the token's acquirer.
|
||||
acquirer_id = vals.get('acquirer_id')
|
||||
acquirer = False
|
||||
payment_token_id = vals.get('payment_token_id')
|
||||
|
||||
if payment_token_id:
|
||||
payment_token = self.env['payment.token'].sudo().browse(payment_token_id)
|
||||
|
||||
# Check payment_token/acquirer matching or take the acquirer from token
|
||||
if acquirer_id:
|
||||
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
|
||||
if payment_token and payment_token.acquirer_id != acquirer:
|
||||
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
|
||||
payment_token.acquirer_id.name, acquirer.name))
|
||||
if payment_token and payment_token.partner_id != partner:
|
||||
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
|
||||
payment_token.partner.name, partner.name))
|
||||
else:
|
||||
acquirer = payment_token.acquirer_id
|
||||
|
||||
# Check an acquirer is there.
|
||||
if not acquirer_id and not acquirer:
|
||||
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
|
||||
|
||||
if not acquirer:
|
||||
acquirer = self.env['payment.acquirer'].browse(acquirer_id)
|
||||
|
||||
# Check a journal is set on acquirer.
|
||||
if not acquirer.journal_id:
|
||||
raise ValidationError(_('A journal must be specified of the acquirer %s.' % acquirer.name))
|
||||
|
||||
if not acquirer_id and acquirer:
|
||||
vals['acquirer_id'] = acquirer.id
|
||||
|
||||
vals.update({
|
||||
'amount': sum(self.mapped('amount_total')),
|
||||
'currency_id': currency.id,
|
||||
'partner_id': partner.id,
|
||||
'sale_order_ids': [(6, 0, self.ids)],
|
||||
})
|
||||
|
||||
transaction = self.env['payment.transaction'].create(vals)
|
||||
|
||||
# Process directly if payment_token
|
||||
if transaction.payment_token_id:
|
||||
transaction.s2s_do_transaction()
|
||||
|
||||
return transaction
|
||||
|
||||
def _force_lines_to_invoice_policy_order(self):
|
||||
for line in self.order_line:
|
||||
if self.state in ['sale', 'done']:
|
||||
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
||||
else:
|
||||
line.qty_to_invoice = 0
|
||||
|
||||
@api.multi
|
||||
def _prepare_invoice(self):
|
||||
# Override
|
||||
# Add the transactions in the SO to the invoices.
|
||||
invoice_vals = super(SaleOrder, self)._prepare_invoice()
|
||||
invoice_vals['transaction_ids'] = [(6, 0, self.transaction_ids.ids)]
|
||||
return invoice_vals
|
||||
|
||||
@api.multi
|
||||
def payment_action_capture(self):
|
||||
self.authorized_transaction_ids.s2s_capture_transaction()
|
||||
|
||||
@api.multi
|
||||
def payment_action_void(self):
|
||||
self.authorized_transaction_ids.s2s_void_transaction()
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.sale.payment</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='sales_settings_invoicing_policy']" position="after">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box"
|
||||
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="automatic_invoice"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="automatic_invoice"/>
|
||||
<div class="text-muted">
|
||||
Generate the invoice automatically when the electronic payment is confirmed
|
||||
</div>
|
||||
<div attrs="{'invisible': [('automatic_invoice','=',False)]}">
|
||||
<label for="template_id" class="o_light_label"/>
|
||||
<field name="template_id" class="oe_inline" options="{'no_create': True}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="sale_order_view_form_inherit_sale_payment" model="ir.ui.view">
|
||||
<field name="name">sale.order.view.form.inherit.sale.payment</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!--
|
||||
Allow the user to capture/void the authorized transactions in the sale order.
|
||||
-->
|
||||
<xpath expr="//button[1]" position="before">
|
||||
<field name="authorized_transaction_ids" invisible="1"/>
|
||||
<button name="payment_action_capture" type="object"
|
||||
string="Capture Transaction" class="oe_highlight"
|
||||
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
|
||||
<button name="payment_action_void" type="object"
|
||||
string="Void Transaction"
|
||||
confirm="Are you sure you want to void the authorized transaction? This action can't be undone."
|
||||
attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,111 +0,0 @@
|
||||
<odoo>
|
||||
<template id="portal_my_orders_payment" name="Payment on my orders" inherit_id="sale.portal_order_page">
|
||||
<xpath expr="//t[@name='portal_confirmation_sign']" position="after">
|
||||
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
|
||||
<t t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and payment_tx_id.state != 'done'" name="portal_confirmation_pay">
|
||||
<a class="btn btn-primary ml8 float-right" href="#portal_pay" role="button">
|
||||
<i class="fa fa-arrow-circle-right"/> Pay Now
|
||||
</a>
|
||||
</t>
|
||||
<t t-if="invoices and order.state in ('sale', 'done') and portal_confirmation == 'pay'">
|
||||
<a role="button" class="btn btn-primary ml8 float-right" disabled="disabled">
|
||||
<i class="fa fa-check-circle"/> Done
|
||||
</a>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-call='sale.portal_order_success']" position="after">
|
||||
<div t-if="order.transaction_ids and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" class="o_sale_payment_tx_status" t-att-data-order-id="order.id">
|
||||
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
|
||||
<t t-call="payment.payment_confirmation_status"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='portal_sign']" position="after">
|
||||
<div class="row" t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.get_portal_last_transaction().state != 'done'" id="portal_pay">
|
||||
<div class="offset-lg-3 col-lg-6">
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
|
||||
<h4 class="mb24">Pay with</h4>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="partner_id" t-value="order.partner_id.id"/>
|
||||
<t t-set="access_token" t-value="access_token"/>
|
||||
<t t-set="success_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
|
||||
<t t-set="error_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
|
||||
<t t-set="callback_method" t-value="''"/>
|
||||
<t t-set="submit_txt" t-value="'Pay Now'"/>
|
||||
<t t-set="icon_class" t-value="'fa-lock'"/>
|
||||
<t t-set="form_action" t-value="'/pay/sale/' + str(order.id) + '/s2s_token_tx/'"/>
|
||||
<t t-set="prepare_tx_url" t-value="'/pay/sale/' + str(order.id) + '/form_tx/'"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" t-if="existing_token"><!-- FIXME -->
|
||||
<div class="offset-lg-3 col-lg-6">
|
||||
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='invoices']" position="before">
|
||||
<t t-set="tx_ids" t-value="order.transaction_ids.filtered(lambda tx: tx.state != 'draft')"/>
|
||||
<t t-if="tx_ids">
|
||||
<div>
|
||||
<strong>Transactions</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-foreach="tx_ids" t-as="tx">
|
||||
<div>
|
||||
<span t-esc="tx.reference"/>
|
||||
<span class="text-muted" t-field="tx.create_date"/>
|
||||
<t t-if="tx.state == 'done'">
|
||||
<span class="badge badge-success orders_label_text_align"><i class="fa fa-fw fa-check"/> Done</span>
|
||||
</t>
|
||||
<t t-if="tx.state != 'done'">
|
||||
<span class="badge badge-info orders_label_text_align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
|
||||
<t t-if="tx.state_message"><br /><span t-esc="tx.state_message"/></t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_error" name="Order error display: payment errors"
|
||||
inherit_id="sale.portal_order_error">
|
||||
<xpath expr="//t[@name='generic']" position="after">
|
||||
<t t-set="tx_id" t-value="order.get_portal_last_transaction()"/>
|
||||
<t t-if="error == 'pay_sale_invalid_doc'">
|
||||
There was an error processing your payment: invalid order.
|
||||
</t>
|
||||
<t t-if="error == 'pay_sale_invalid_token'">
|
||||
There was en error processing your payment: invalid credit card ID.
|
||||
</t>
|
||||
<t t-if="error == 'pay_sale_tx_fail'">
|
||||
There was an error processing your payment: transaction failed.<br />
|
||||
<t t-if="tx_id and tx_id.state_message">
|
||||
<t t-esc="tx_id.state_message"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="error == 'pay_sale_tx_state'">
|
||||
There was an error processing your payment: transaction issue.<br />
|
||||
<t t-if="tx_id and tx_id.state_message">
|
||||
<t t-esc="tx_id.state_message"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="error == 'pay_sale_tx_token'">
|
||||
There was an error processing your payment: issue with credit card ID validation.
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_success" name="Order success display: payment success"
|
||||
inherit_id="sale.portal_order_success">
|
||||
<xpath expr="//a[hasclass('close')]" position="after">
|
||||
<t t-if="success == 'pay_sale'">
|
||||
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
|
||||
<span t-if='payment_tx_id and payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
|
||||
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.sale_payment</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//label[@for='portal_confirmation_pay']/.." position="inside">
|
||||
<div attrs="{'invisible': [('portal_confirmation_pay', '=', False)]}">
|
||||
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Quotation Builder',
|
||||
'category': 'Website',
|
||||
'summary': 'Sales',
|
||||
'website': 'https://www.odoo.com/page/quote-builder',
|
||||
'version': '1.0',
|
||||
'description': "",
|
||||
'depends': ['website', 'sale_management', 'website_mail'],
|
||||
'data': [
|
||||
'data/sale_order_template_data.xml',
|
||||
'views/sale_portal_templates.xml',
|
||||
'views/sale_order_template_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment
|
||||
from . import portal
|
||||
from . import portal
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
@http.route(["/sale_quotation_builder/template/<model('sale.order.template'):template>"], type='http', auth="user", website=True)
|
||||
def sale_quotation_builder_template_view(self, template, **post):
|
||||
values = {'template': template}
|
||||
return request.render('sale_quotation_builder.so_template', values)
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sale_management.sale_order_template_default" model="sale.order.template">
|
||||
<field name="website_description" type="xml">
|
||||
<section data-snippet-id="title">
|
||||
<h1 class="page-header">About us</h1>
|
||||
</section>
|
||||
<section data-snippet-id="text-block">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>
|
||||
This is a <strong>sample quotation template</strong>. You should
|
||||
customize it to fit your own needs from the <i>Sale</i>
|
||||
application, using the menu: Configuration /
|
||||
Quotation Templates.
|
||||
</p><p>
|
||||
Great quotation templates will significantly
|
||||
<strong>boost your success rate</strong>. The
|
||||
first section is usually about your company,
|
||||
your references, your methodology or
|
||||
guarantees, your team, SLA, terms and conditions, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section data-snippet-id="quality">
|
||||
<div class="row mt32">
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Our Quality</h3>
|
||||
<div class="card-body">
|
||||
Product quality is the foundation we
|
||||
stand on; we build it with a relentless
|
||||
focus on fabric, performance and craftsmanship.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Our Service</h3>
|
||||
<div class="card-body">
|
||||
As a leading professional services firm,
|
||||
we know that success is all about the
|
||||
commitment we put on strong services.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Price</h3>
|
||||
<div class="card-body">
|
||||
We always ensure that our products are
|
||||
set at a fair price so that you will be
|
||||
happy to buy them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section data-snippet-id="text-block">
|
||||
<h1 class="page-header">Our Offer</h1>
|
||||
<p>
|
||||
You can <strong>set a description per product</strong>. Odoo will
|
||||
automatically create a quotation using the descriptions
|
||||
of all products in the proposal. The table of content
|
||||
on the left is generated automatically using the styles you
|
||||
used in your description (heading 1, heading 2, ...)
|
||||
</p><p>
|
||||
If edit a quotation from the 'Preview' of a quotation, you will
|
||||
update the quotation for this customer only. If you edit the quotation
|
||||
template (from the Configuration menu), all future quotations will
|
||||
use this modified template.
|
||||
</p>
|
||||
</section>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
from . import product_template
|
||||
from . import sale_order
|
||||
from . import sale_order_template
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
quotation_only_description = fields.Html('Quotation Only Description', sanitize_attributes=False,
|
||||
translate=html_translate, help="The quotation description (not used on eCommerce)")
|
||||
|
||||
quotation_description = fields.Html('Quotation Description', compute='_compute_quotation_description',
|
||||
help="This field uses the Quotation Only Description if it is defined, otherwise it will try to read the eCommerce Description.")
|
||||
|
||||
@api.multi
|
||||
def _compute_quotation_description(self):
|
||||
for record in self:
|
||||
if record.quotation_only_description:
|
||||
record.quotation_description = record.quotation_only_description
|
||||
elif hasattr(record, 'website_description') and record.website_description:
|
||||
record.quotation_description = record.website_description
|
||||
else:
|
||||
record.quotation_description = ''
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate)
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def onchange_update_description_lang(self):
|
||||
if not self.sale_order_template_id:
|
||||
return
|
||||
else:
|
||||
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
|
||||
self.website_description = template.website_description
|
||||
|
||||
def _compute_line_data_for_template_change(self, line):
|
||||
vals = super(SaleOrder, self)._compute_line_data_for_template_change(line)
|
||||
vals.update(website_description=line.website_description)
|
||||
return vals
|
||||
|
||||
def _compute_option_data_for_template_change(self, option):
|
||||
vals = super(SaleOrder, self)._compute_option_data_for_template_change(option)
|
||||
vals.update(website_description=option.website_description)
|
||||
return vals
|
||||
|
||||
@api.onchange('sale_order_template_id')
|
||||
def onchange_sale_order_template_id(self):
|
||||
ret = super(SaleOrder, self).onchange_sale_order_template_id()
|
||||
if self.sale_order_template_id:
|
||||
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
|
||||
self.website_description = template.website_description
|
||||
return ret
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
website_description = fields.Html('Website Description', sanitize=False, translate=html_translate)
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
values = self._inject_quotation_description(values)
|
||||
return super(SaleOrderLine, self).create(values)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
values = self._inject_quotation_description(values)
|
||||
return super(SaleOrderLine, self).write(values)
|
||||
|
||||
def _inject_quotation_description(self, values):
|
||||
values = dict(values or {})
|
||||
if not values.get('website_description') and values.get('product_id'):
|
||||
product = self.env['product.product'].browse(values['product_id'])
|
||||
values.update(website_description=product.quotation_description)
|
||||
return values
|
||||
|
||||
|
||||
class SaleOrderOption(models.Model):
|
||||
_inherit = "sale.order.option"
|
||||
|
||||
website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate)
|
||||
|
||||
@api.onchange('product_id', 'uom_id')
|
||||
def _onchange_product_id(self):
|
||||
ret = super(SaleOrderOption, self)._onchange_product_id()
|
||||
if self.product_id:
|
||||
product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
|
||||
self.website_description = product.quotation_description
|
||||
return ret
|
||||
|
||||
@api.multi
|
||||
def _get_values_to_add_to_order(self):
|
||||
values = super(SaleOrderOption, self)._get_values_to_add_to_order()
|
||||
values.update(website_description=self.website_description)
|
||||
return values
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class SaleOrderTemplate(models.Model):
|
||||
_inherit = "sale.order.template"
|
||||
|
||||
website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False)
|
||||
|
||||
@api.multi
|
||||
def open_template(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'self',
|
||||
'url': '/sale_quotation_builder/template/%d' % self.id
|
||||
}
|
||||
|
||||
|
||||
class SaleOrderTemplateLine(models.Model):
|
||||
_inherit = "sale.order.template.line"
|
||||
|
||||
website_description = fields.Html('Website Description', related='product_id.product_tmpl_id.quotation_only_description', translate=html_translate)
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
ret = super(SaleOrderTemplateLine, self)._onchange_product_id()
|
||||
if self.product_id:
|
||||
self.website_description = self.product_id.quotation_description
|
||||
return ret
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
values = self._inject_quotation_description(values)
|
||||
return super(SaleOrderTemplateLine, self).create(values)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
values = self._inject_quotation_description(values)
|
||||
return super(SaleOrderTemplateLine, self).write(values)
|
||||
|
||||
def _inject_quotation_description(self, values):
|
||||
values = dict(values or {})
|
||||
if not values.get('website_description') and values.get('product_id'):
|
||||
product = self.env['product.product'].browse(values['product_id'])
|
||||
values['website_description'] = product.quotation_description
|
||||
return values
|
||||
|
||||
|
||||
class SaleOrderTemplateOption(models.Model):
|
||||
_inherit = "sale.order.template.option"
|
||||
|
||||
website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False)
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
ret = super(SaleOrderTemplateOption, self)._onchange_product_id()
|
||||
if self.product_id:
|
||||
self.website_description = self.product_id.quotation_description
|
||||
return ret
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="sale_order_template_view_form_inherit_sale_quotation_builder" model="ir.ui.view">
|
||||
<field name="name">sale.order.template.form.inherit.sale_quotation_builder</field>
|
||||
<field name="inherit_id" ref="sale_management.sale_order_template_view_form"/>
|
||||
<field name="model">sale.order.template</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button string="Edit Template" type="object" name="open_template" class="oe_stat_button" icon="fa-pencil"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//notebook[@name='description']" position="inside">
|
||||
<page string="Website Description">
|
||||
<field name="website_description" />
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//tree/field[@name='price_unit']" position="after">
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//notebook[@name='main_book']" position="inside">
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="sale_order_form_quote_design" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.sale_quotation_builder</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale_management.sale_order_form_quote"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//button[@name='button_add_to_order']" position="after">
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='require_payment']" position="after">
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="sale_order_portal_content_inherit_sale_quotation_builder" name="Order Design" inherit_id="sale.sale_order_portal_content">
|
||||
<xpath expr="//div[@id='informations']" position="after">
|
||||
<div t-field="sale_order.website_description" class="oe_no_empty"/>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="sale_order.order_line" t-as="line">
|
||||
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
|
||||
<a t-att-id="line.id"/>
|
||||
<div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Template to edit the quotation template with the website editor -->
|
||||
<template id="so_template" name="SO Template">
|
||||
<t t-call="website.layout">
|
||||
<body data-spy="scroll" data-target=".navspy" data-offset="50">
|
||||
<div class="container o_sale_order">
|
||||
<div class="row mt16">
|
||||
<div class="col-lg-3 d-print-none">
|
||||
<div class="bs-sidebar">
|
||||
<div class="navspy" role="complementary" t-ignore="True">
|
||||
<ul class="nav bs-sidenav" data-id="quote_sidebar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="row mb8">
|
||||
<div t-ignore="true" class="css_editable_mode_hidden offset-lg-11">
|
||||
<a role="button" class="btn btn-info d-print-none" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (template._name, template.id, request.env.ref('sale_management.sale_order_template_action').id)">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info alert-dismissable" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<p>
|
||||
<strong>Template Header:</strong> this content
|
||||
will appear on all quotations using this
|
||||
template.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
Titles with style <i>Heading 1</i> and
|
||||
<i>Heading 2</i> will be used to generate the
|
||||
table of content automatically.
|
||||
</p>
|
||||
</div>
|
||||
<div id="template_introduction" t-field="template.website_description" class="oe_no_empty"/>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="template.sale_order_template_line_ids" t-as="line">
|
||||
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
|
||||
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Product: <strong t-esc="line.product_id.name"/>:
|
||||
this content will appear on the quotation only if this
|
||||
product is put on the quote.
|
||||
</div>
|
||||
<div t-field="line.website_description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="template.sale_order_template_option_ids" t-as="option_line">
|
||||
<t t-if="option_line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [option_line.product_id.product_tmpl_id.id]"/>
|
||||
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Optional Product: <strong t-esc="option_line.product_id.name"/>:
|
||||
this content will appear on the quotation only if this
|
||||
product is used in the quote.
|
||||
</div>
|
||||
<div t-field="option_line.website_description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
<section id="terms" class="container" t-if="template.note">
|
||||
<h1 t-ignore="True">Terms & Conditions</h1>
|
||||
<p t-field="template.note"/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="sale.brand_promotion" inherit_id="website.brand_promotion">
|
||||
<xpath expr="//div[hasclass('o_brand_promotion')]" position="replace">
|
||||
<div class="o_brand_promotion">
|
||||
Powered by <a target="_blank" class="badge badge-danger" href="http://www.odoo.com/page/website-builder">Odoo</a>,
|
||||
an awesome <a target="_blank" href="http://www.odoo.com/page/crm">Open Source CRM</a>.
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="portal_order_page_shipping" name="Orders Shipping Followup" inherit_id="sale.portal_order_page">
|
||||
<template id="sale_order_portal_content_inherit_sale_stock" name="Orders Shipping Followup" inherit_id="sale.sale_order_portal_content">
|
||||
<xpath expr="//div[@id='shipping_address']" position="inside">
|
||||
<t t-if="order.picking_ids">
|
||||
<t t-if="sale_order.picking_ids">
|
||||
<div>
|
||||
<strong>Delivery Orders</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-foreach="order.picking_ids.filtered(lambda picking: picking.picking_type_id.code != 'internal')" t-as="i">
|
||||
<t t-set="report_url" t-value="'/my/picking/pdf/%s?%s' % (i.id, keep_query())"/>
|
||||
<t t-foreach="sale_order.picking_ids.filtered(lambda picking: picking.picking_type_id.code != 'internal')" t-as="i">
|
||||
<t t-set="delivery_report_url" t-value="'/my/picking/pdf/%s?%s' % (i.id, keep_query())"/>
|
||||
<div id='picking_info'>
|
||||
<a t-att-href="report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
|
||||
<a t-att-href="report_url"><span t-field="i.name"/></a>
|
||||
<a t-att-href="delivery_report_url"><span class="fa fa-download" role="img" aria-label="Download" title="Download"/></a>
|
||||
<a t-att-href="delivery_report_url"><span t-field="i.name"/></a>
|
||||
<span class="text-muted" t-field="i.date" t-options='{"widget": "date"}'/>
|
||||
<t t-if="i.state == 'done'">
|
||||
<span class="badge badge-success label-text-align"><i class="fa fa-fw fa-truck"/> Shipped</span>
|
||||
|
||||
@@ -15,6 +15,8 @@ class TestReInvoice(TestCommonSaleTimesheetNoChart):
|
||||
cls.setUpAdditionalAccounts()
|
||||
cls.setUpAccountJournal()
|
||||
|
||||
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
|
||||
|
||||
# patch expense products to make them services creating task/project
|
||||
service_values = {
|
||||
'type': 'service',
|
||||
|
||||
@@ -15,6 +15,8 @@ class TestReporting(TestCommonSaleTimesheetNoChart):
|
||||
cls.setUpAdditionalAccounts()
|
||||
cls.setUpAccountJournal()
|
||||
|
||||
cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id
|
||||
|
||||
# expense product
|
||||
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'name': "Expense service",
|
||||
|
||||
@@ -14,7 +14,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
return self.env['website'].search([], limit=1)
|
||||
|
||||
website_id = fields.Many2one('website', string="website",
|
||||
default=_default_website, required=True, ondelete='cascade')
|
||||
default=_default_website, ondelete='cascade')
|
||||
website_name = fields.Char('Website Name', related='website_id.name')
|
||||
website_domain = fields.Char('Website Domain', related='website_id.domain')
|
||||
website_country_group_ids = fields.Many2many(related='website_id.country_group_ids')
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
Send Quotes Online
|
||||
------------------
|
||||
|
||||
### Create polished, professional quotes in minutes
|
||||
|
||||
|
||||
Use templates to create polished, professional quotes in minutes. Send these quotes by email and let your customer sign online. Use cross-selling and discounts to push boost your sales.
|
||||
|
||||
Electronic Signature
|
||||
--------------------
|
||||
|
||||
### No more fax or scans in emails
|
||||
|
||||
Let your customers sign proposals online. Use up-selling techniques to propose them some options.
|
||||
|
||||
Spend the extra time focusing on selling, not recording data.
|
||||
|
||||
Communicate Efficiently
|
||||
-----------------------
|
||||
|
||||
Have clear pricing strategies.
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Online Proposals',
|
||||
'category': 'Sales',
|
||||
'summary': 'Build quotation templates',
|
||||
'website': 'https://www.odoo.com/page/quote-builder',
|
||||
'version': '1.0',
|
||||
'description': "This module allows to create polished, professional proposals using templates. Templates come with default products, options, terms & conditions and confirmation methods (sign, pay).",
|
||||
'depends': ['website', 'sale_management', 'mail', 'sale_payment', 'website_mail'],
|
||||
'data': [
|
||||
'data/website_quote_data.xml',
|
||||
'report/sale_order_reports.xml',
|
||||
'report/sale_order_templates.xml',
|
||||
'report/website_quote_templates.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/sale_quote_views.xml',
|
||||
'views/website_quote_templates.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'demo': [
|
||||
'data/website_quote_demo.xml'
|
||||
],
|
||||
'qweb': ['static/src/xml/*.xml'],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import exceptions, fields, http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import get_records_pager
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
from odoo.addons.portal.controllers.mail import _message_post_helper
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
@http.route()
|
||||
def portal_order_page(self, order_id=None, access_token=None, **kw):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except exceptions.AccessError:
|
||||
pass
|
||||
else:
|
||||
if order_sudo.template_id and order_sudo.template_id.active:
|
||||
return request.redirect('/quote/%s/%s' % (order_id, access_token or ''))
|
||||
return super(CustomerPortal, self).portal_order_page(order_id=order_id, access_token=access_token, **kw)
|
||||
|
||||
def _portal_quote_user_can_accept(self, order):
|
||||
result = super(CustomerPortal, self)._portal_quote_user_can_accept(order)
|
||||
# either use quote template settings or fallback on default behavior
|
||||
return order.require_signature if order.template_id else result
|
||||
|
||||
|
||||
class sale_quote(http.Controller):
|
||||
|
||||
@http.route("/quote/<int:order_id>", type='http', auth="user", website=True)
|
||||
def view_user(self, *args, **kwargs):
|
||||
return self.view(*args, **kwargs)
|
||||
|
||||
@http.route("/quote/<int:order_id>/<token>", type='http', auth="public", website=True)
|
||||
def view(self, order_id, pdf=None, token=None, message=False, **post):
|
||||
# use sudo to allow accessing/viewing orders for public user
|
||||
# only if he knows the private token
|
||||
now = fields.Date.today()
|
||||
if token:
|
||||
Order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)])
|
||||
else:
|
||||
Order = request.env['sale.order'].search([('id', '=', order_id)])
|
||||
# Log only once a day
|
||||
if Order and request.session.get('view_quote_%s' % Order.id) != now and request.env.user.share and token:
|
||||
request.session['view_quote_%s' % Order.id] = now
|
||||
body = _('Quotation viewed by customer')
|
||||
_message_post_helper(res_model='sale.order', res_id=Order.id, message=body, token=Order.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=Order.user_id.sudo().partner_id.ids)
|
||||
if not Order:
|
||||
return request.render('website.404')
|
||||
|
||||
# Token or not, sudo the order, since portal user has not access on
|
||||
# taxes, required to compute the total_amout of SO.
|
||||
order_sudo = Order.sudo()
|
||||
|
||||
days = 0
|
||||
if order_sudo.validity_date:
|
||||
days = (fields.Date.from_string(order_sudo.validity_date) - fields.Date.from_string(fields.Date.today())).days + 1
|
||||
if pdf:
|
||||
pdf = request.env.ref('website_quote.report_web_quote').sudo().with_context(set_viewport_size=True).render_qweb_pdf([order_sudo.id])[0]
|
||||
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
|
||||
return request.make_response(pdf, headers=pdfhttpheaders)
|
||||
transaction = order_sudo.get_portal_last_transaction()
|
||||
values = {
|
||||
'quotation': order_sudo,
|
||||
'message': message and int(message) or False,
|
||||
'option': any(not x.line_id for x in order_sudo.options),
|
||||
'order_valid': (not order_sudo.validity_date) or (now <= order_sudo.validity_date),
|
||||
'days_valid': days,
|
||||
'action': request.env.ref('sale.action_quotations').id,
|
||||
'no_breadcrumbs': request.env.user.partner_id.commercial_partner_id not in order_sudo.message_partner_ids,
|
||||
'tx_id': transaction.id if transaction else False,
|
||||
'tx_state': transaction.state if transaction else False,
|
||||
'payment_tx': transaction,
|
||||
'tx_post_msg': transaction.acquirer_id.post_msg if transaction else False,
|
||||
'need_payment': order_sudo.invoice_status == 'to invoice' and transaction.state in ['draft', 'cancel'],
|
||||
'token': token,
|
||||
'return_url': '/shop/payment/validate',
|
||||
'bootstrap_formatting': True,
|
||||
'partner_id': order_sudo.partner_id.id,
|
||||
}
|
||||
|
||||
if order_sudo.has_to_be_paid() or values['need_payment']:
|
||||
domain = expression.AND([
|
||||
['&', ('website_published', '=', True), ('company_id', '=', order_sudo.company_id.id)],
|
||||
['|', ('specific_countries', '=', False), ('country_ids', 'in', [order_sudo.partner_id.country_id.id])]
|
||||
])
|
||||
acquirers = request.env['payment.acquirer'].sudo().search(domain)
|
||||
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
|
||||
values['pms'] = request.env['payment.token'].search(
|
||||
[('partner_id', '=', order_sudo.partner_id.id),
|
||||
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
|
||||
|
||||
history = request.session.get('my_quotes_history', [])
|
||||
values.update(get_records_pager(history, order_sudo))
|
||||
return request.render('website_quote.so_quotation', values)
|
||||
|
||||
@http.route(['/quote/<int:order_id>/<token>/decline'], type='http', auth="public", methods=['POST'], website=True)
|
||||
def decline(self, order_id, token, **post):
|
||||
Order = request.env['sale.order'].sudo().browse(order_id)
|
||||
if token != Order.access_token:
|
||||
return request.render('website.404')
|
||||
if Order.state != 'sent':
|
||||
return werkzeug.utils.redirect("/quote/%s/%s?message=4" % (order_id, token))
|
||||
Order.action_cancel()
|
||||
message = post.get('decline_message')
|
||||
if message:
|
||||
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': token} if token else {})
|
||||
return werkzeug.utils.redirect("/quote/%s/%s?message=2" % (order_id, token))
|
||||
|
||||
@http.route(['/quote/update_line'], type='json', auth="public", website=True)
|
||||
def update(self, line_id, remove=False, unlink=False, order_id=None, token=None, **post):
|
||||
Order = request.env['sale.order'].sudo().browse(int(order_id))
|
||||
if token != Order.access_token:
|
||||
return request.render('website.404')
|
||||
if Order.state not in ('draft', 'sent'):
|
||||
return False
|
||||
OrderLine = request.env['sale.order.line'].sudo().browse(int(line_id))
|
||||
if unlink:
|
||||
OrderLine.unlink()
|
||||
return False
|
||||
number = -1 if remove else 1
|
||||
quantity = OrderLine.product_uom_qty + number
|
||||
OrderLine.write({'product_uom_qty': quantity})
|
||||
return [str(quantity), str(Order.amount_total)]
|
||||
|
||||
@http.route(["/quote/template/<model('sale.quote.template'):quote>"], type='http', auth="user", website=True)
|
||||
def template_view(self, quote, **post):
|
||||
values = {'template': quote}
|
||||
return request.render('website_quote.so_template', values)
|
||||
|
||||
@http.route(["/quote/add_line/<int:option_id>/<int:order_id>/<token>"], type='http', auth="public", website=True)
|
||||
def add(self, option_id, order_id, token, **post):
|
||||
Order = request.env['sale.order'].sudo().browse(order_id)
|
||||
if token != Order.access_token:
|
||||
return request.render('website.404')
|
||||
if Order.state not in ['draft', 'sent']:
|
||||
return request.render('website.http_error', {'status_code': 'Forbidden', 'status_message': _('You cannot add options to a confirmed order.')})
|
||||
Option = request.env['sale.order.option'].sudo().browse(option_id)
|
||||
vals = {
|
||||
'price_unit': Option.price_unit,
|
||||
'website_description': Option.website_description,
|
||||
'name': Option.name,
|
||||
'order_id': Order.id,
|
||||
'product_id': Option.product_id.id,
|
||||
'product_uom_qty': Option.quantity,
|
||||
'product_uom': Option.uom_id.id,
|
||||
'discount': Option.discount,
|
||||
}
|
||||
|
||||
OrderLine = request.env['sale.order.line'].sudo().create(vals)
|
||||
OrderLine._compute_tax_id()
|
||||
Option.write({'line_id': OrderLine.id})
|
||||
return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (Order.id, token))
|
||||
|
||||
# note dbo: website_sale code
|
||||
@http.route(['/quote/<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:
|
||||
return False
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'type': order._get_payment_type(),
|
||||
}
|
||||
|
||||
transaction = order._create_payment_transaction(vals)
|
||||
|
||||
return transaction.render_sale_button(
|
||||
order,
|
||||
'/quote/%s/%s' % (order_id, access_token) if access_token else '/quote/%s' % order_id,
|
||||
submit_txt=_('Pay & Confirm'),
|
||||
render_values={
|
||||
'type': order._get_payment_type(),
|
||||
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
|
||||
}
|
||||
)
|
||||
|
||||
@http.route('/quote/<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 or not order.order_line or pm_id is None:
|
||||
return request.redirect("/quote/%s" % order_id)
|
||||
|
||||
# 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('/quote/%s' % order_id)
|
||||
|
||||
# Create transaction
|
||||
vals = {
|
||||
'payment_token_id': pm_id,
|
||||
'type': 'server2server',
|
||||
}
|
||||
|
||||
order._create_payment_transaction(vals)
|
||||
|
||||
return request.redirect('/quote/%s/%s' % (order_id, order.access_token))
|
||||
@@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Mails & quote templates is done in a NOUPDATE block
|
||||
so users can freely customize/delete them -->
|
||||
<data noupdate="1">
|
||||
<record id="website_quote_template_default" model="sale.quote.template">
|
||||
<field name="name">Default Template</field>
|
||||
<field name="number_of_days">30</field>
|
||||
<field name="website_description" type="xml">
|
||||
<section data-snippet-id="title">
|
||||
<h1 class="page-header">About us</h1>
|
||||
</section>
|
||||
<section data-snippet-id="text-block">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>
|
||||
This is a <strong>sample quotation template</strong>. You should
|
||||
customize it to fit your own needs from the <i>Sale</i>
|
||||
application, using the menu: Configuration /
|
||||
Quotation Templates.
|
||||
</p><p>
|
||||
Great quotation templates will significantly
|
||||
<strong>boost your success rate</strong>. The
|
||||
first section is usually about your company,
|
||||
your references, your methodology or
|
||||
guarantees, your team, SLA, terms and conditions, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section data-snippet-id="quality">
|
||||
<div class="row mt32">
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Our Quality</h3>
|
||||
<div class="card-body">
|
||||
Product quality is the foundation we
|
||||
stand on; we build it with a relentless
|
||||
focus on fabric, performance and craftsmanship.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Our Service</h3>
|
||||
<div class="card-body">
|
||||
As a leading professional services firm,
|
||||
we know that success is all about the
|
||||
commitment we put on strong services.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-info text-white">
|
||||
<h3 class="card-header text-center">Price</h3>
|
||||
<div class="card-body">
|
||||
We always ensure that our products are
|
||||
set at a fair price so that you will be
|
||||
happy to buy them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section data-snippet-id="text-block">
|
||||
<h1 class="page-header">Our Offer</h1>
|
||||
<p>
|
||||
You can <strong>set a description per product</strong>. Odoo will
|
||||
automatically create a quotation using the descriptions
|
||||
of all products in the proposal. The table of content
|
||||
on the left is generated automatically using the styles you
|
||||
used in your description (heading 1, heading 2, ...)
|
||||
</p><p>
|
||||
If edit a quotation from the 'Preview' of a quotation, you will
|
||||
update the quotation for this customer only. If you edit the quotation
|
||||
template (from the Configuration menu), all future quotations will
|
||||
use this modified template.
|
||||
</p>
|
||||
</section>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,7 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from .import sale_order
|
||||
from .import sale_quote
|
||||
from .import product_template
|
||||
from .import res_config_settings
|
||||
@@ -1,12 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
website_description = fields.Html('Description for the website', sanitize_attributes=False) # hack, if website_sale is not installed
|
||||
quote_description = fields.Html('Description for the quote', sanitize_attributes=False, translate=html_translate)
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
default_template_id = fields.Many2one('sale.quote.template', default_model='sale.order', string='Default Template')
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<report
|
||||
id="report_web_quote"
|
||||
string="Web Quotation"
|
||||
model="sale.order"
|
||||
report_type="qweb-pdf"
|
||||
file="website_quote.report_quotation"
|
||||
name="website_quote.report_quote"
|
||||
menu="False"
|
||||
print_report_name="
|
||||
(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name or
|
||||
'Order - %s' % (object.name))"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_quote">
|
||||
<t t-call="web.html_container">
|
||||
<t t-set="data_report_margin_top" t-value="45"/>
|
||||
<t t-set="data_report_header_spacing" t-value="40"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="o" t-value="docs[0]"/>
|
||||
<t t-foreach="docs" t-as="quotation">
|
||||
<div class="page">
|
||||
<t t-call="website_quote.so_quotation_content"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -1,9 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_quote_template,sale.quote.template,model_sale_quote_template,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_quote_template_manager,sale.quote.template,model_sale_quote_template,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_quote_line,sale.quote.line,model_sale_quote_line,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_quote_line_manager,sale.quote.line,model_sale_quote_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_quote_option,sale.quote.option,model_sale_quote_option,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_sale_quote_option_manager,sale.quote.option,model_sale_quote_option,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_order_option,sale.order.option,model_sale_order_option,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_order_option_all,sale.order.option,model_sale_order_option,,1,0,0,0
|
||||
|
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#7CC098"/><stop offset="100%" stop-color="#5F8A71"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M44.923 69H4c-2 0-4-1-4-4V39.096L16.68 18H54v27.131L49.097 51h7.862L44.923 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M14 50h43v3H14v-3zm37.167-1.375H19.833c-2.154 0-3.916-1.969-3.916-4.375V22.375c0-2.406 1.762-4.375 3.916-4.375h31.334c2.154 0 3.916 1.969 3.916 4.375V44.25c0 2.406-1.762 4.375-3.916 4.375zM18.875 21.187v24.25h33.25v-24.25h-33.25zm6.54 9.963c.829 2.053 6.405-.807 7.982 3.096.755 1.869.133 3.977-1.791 5.142l.625 1.547a.414.414 0 0 1-.218.543l-1.253.506a.414.414 0 0 1-.534-.24l-.635-1.572c-1.326.281-2.668.132-3.725-.314a.435.435 0 0 1-.25-.5l.417-1.71a.395.395 0 0 1 .509-.287c1.044.337 2.261.447 3.224.058 1.122-.453 1.352-1.355 1.09-2.002-.771-1.91-6.39.843-8.018-3.185-.695-1.719-.045-3.598 1.616-4.773l-.656-1.625a.414.414 0 0 1 .218-.543l1.253-.506a.414.414 0 0 1 .534.24l.622 1.54c1.056-.286 2.314-.316 3.357.077.18.067.293.253.274.446l-.164 1.718a.397.397 0 0 1-.497.353c-.912-.241-1.931-.294-2.768.044-1.044.422-1.52 1.186-1.213 1.947zM37 34h11v2H37v-2zm7 4h4v2h-4v-2zm-7-11h11v2H37v-2zm4 3h7v2h-7v-2z" opacity=".3"/><path fill="#FFF" d="M15.006 48H56c.667 0 1 .333 1 1v2H14v-2c0-.667.335-1 1.006-1zm36.16-1.375H19.834c-2.154 0-3.916-1.969-3.916-4.375V20.375c0-2.406 1.762-4.375 3.916-4.375h31.334c2.154 0 3.916 1.969 3.916 4.375V42.25c0 2.406-1.762 4.375-3.916 4.375zM18.876 19.187v24.25h33.25v-24.25h-33.25zm6.54 9.468c.829 2.053 6.405-.806 7.982 3.096.755 1.869.133 3.978-1.791 5.142l.625 1.547a.414.414 0 0 1-.218.543l-1.253.506a.414.414 0 0 1-.534-.24l-.635-1.571c-1.326.28-2.668.132-3.725-.315a.435.435 0 0 1-.25-.5l.417-1.71a.395.395 0 0 1 .509-.287c1.044.337 2.261.447 3.224.058 1.122-.453 1.352-1.355 1.09-2.001-.771-1.911-6.39.842-8.018-3.186-.695-1.718-.045-3.598 1.616-4.773l-.656-1.624a.414.414 0 0 1 .218-.544l1.253-.506a.414.414 0 0 1 .534.24l.622 1.54c1.056-.286 2.314-.316 3.357.077.18.068.293.253.274.446l-.164 1.718a.397.397 0 0 1-.497.353c-.912-.24-1.931-.294-2.768.044-1.044.422-1.52 1.186-1.213 1.947zM37 32h11v2H37v-2zm7 4h4v2h-4v-2zm-7-11h11v2H37v-2zm4 3h7v2h-7v-2z"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,55 +0,0 @@
|
||||
.o_website_quote .bs-sidebar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index : 1;
|
||||
background-color: #f7f5fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.o_website_quote .bs-sidenav {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.o_website_quote .bs-sidebar .nav > li > a {
|
||||
display: block;
|
||||
color: #716b7a;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.o_website_quote .bs-sidebar .nav > .active > a,
|
||||
.o_website_quote .bs-sidebar .nav > .active:hover > a,
|
||||
.o_website_quote .bs-sidebar .nav > .active:focus > a {
|
||||
font-weight: bold;
|
||||
color: #563d7c;
|
||||
background-color: transparent;
|
||||
border-right: 1px solid #563d7c;
|
||||
}
|
||||
|
||||
.o_website_quote .bs-sidebar .nav .nav {
|
||||
display: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.o_website_quote .bs-sidebar .nav .nav > li > a {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
padding-left: 30px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* Table Styling */
|
||||
.wq-table {
|
||||
table-layout: auto !important;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.o_website_quote .bs-sidebar .nav > .active > ul {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding : 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
odoo.define('website_quote.website_quote', function (require) {
|
||||
'use strict';
|
||||
|
||||
require('web.dom_ready');
|
||||
var ajax = require('web.ajax');
|
||||
var config = require('web.config');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
if(!$('.o_website_quote').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.o_website_quote'");
|
||||
}
|
||||
|
||||
// Add to SO button
|
||||
var UpdateLineButton = Widget.extend({
|
||||
events: {
|
||||
'click' : 'onClick',
|
||||
},
|
||||
onClick: function(ev){
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var href = this.$el.attr("href");
|
||||
var order_id = href.match(/order_id=([0-9]+)/);
|
||||
var line_id = href.match(/update_line\/([0-9]+)/);
|
||||
var token = href.match(/token=(.*)/);
|
||||
ajax.jsonRpc("/quote/update_line", 'call', {
|
||||
'line_id': line_id[1],
|
||||
'order_id': parseInt(order_id[1]),
|
||||
'token': token[1],
|
||||
'remove': self.$el.is('[href*="remove"]'),
|
||||
'unlink': self.$el.is('[href*="unlink"]')
|
||||
}).then(function (data) {
|
||||
if(!data){
|
||||
window.location.reload();
|
||||
}
|
||||
self.$el.parents('.input-group:first').find('.js_quantity').val(data[0]);
|
||||
$('[data-id="total_amount"]>span').html(data[1]);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
var update_button_list = [];
|
||||
$('a.js_update_line_json').each(function( index ) {
|
||||
var button = new UpdateLineButton();
|
||||
button.setElement($(this)).start();
|
||||
update_button_list.push(button);
|
||||
});
|
||||
|
||||
// Nav Menu ScrollSpy
|
||||
var NavigationSpyMenu = Widget.extend({
|
||||
start: function(watched_selector){
|
||||
this.authorized_text_tag = ['em', 'b', 'i', 'u'];
|
||||
this.spy_watched = $(watched_selector);
|
||||
this.generateMenu();
|
||||
},
|
||||
generateMenu: function(){
|
||||
var self = this;
|
||||
// reset ids
|
||||
$("[id^=quote_header_], [id^=quote_]", this.spy_watched).attr("id", "");
|
||||
// generate the new spy menu
|
||||
var last_li = false;
|
||||
var last_ul = null;
|
||||
_.each(this.spy_watched.find("h1, h2"), function(el){
|
||||
var id, text;
|
||||
switch (el.tagName.toLowerCase()) {
|
||||
case "h1":
|
||||
id = self.setElementId('quote_header_', el);
|
||||
text = self.extractText($(el));
|
||||
if (!text) {
|
||||
break;
|
||||
}
|
||||
last_li = $("<li>").append($('<a href="#'+id+'"/>').text(text)).appendTo(self.$el);
|
||||
last_ul = false;
|
||||
break;
|
||||
case "h2":
|
||||
id = self.setElementId('quote_', el);
|
||||
text = self.extractText($(el));
|
||||
if (!text) {
|
||||
break;
|
||||
}
|
||||
if (last_li) {
|
||||
if (!last_ul) {
|
||||
last_ul = $("<ul class='nav'>").appendTo(last_li);
|
||||
}
|
||||
$("<li>").append($('<a href="#'+id+'"/>').text(text)).appendTo(last_ul);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
setElementId: function(prefix, $el){
|
||||
var id = _.uniqueId(prefix);
|
||||
this.spy_watched.find($el).attr('id', id);
|
||||
return id;
|
||||
},
|
||||
extractText: function($node){
|
||||
var self = this;
|
||||
var raw_text = [];
|
||||
_.each($node.contents(), function(el){
|
||||
var current = $(el);
|
||||
if($.trim(current.text())){
|
||||
var tagName = current.prop("tagName");
|
||||
if(_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorized_text_tag, tagName.toLowerCase()))){
|
||||
raw_text.push($.trim(current.text()));
|
||||
}
|
||||
}
|
||||
});
|
||||
return raw_text.join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
var nav_menu = new NavigationSpyMenu();
|
||||
nav_menu.setElement($('[data-id="quote_sidebar"]'));
|
||||
nav_menu.start($('body[data-target=".navspy"]'));
|
||||
|
||||
var $bs_sidebar = $(".o_website_quote .bs-sidebar");
|
||||
$(window).on('resize', _.throttle(adapt_sidebar_position, 200, {leading: false}));
|
||||
adapt_sidebar_position();
|
||||
|
||||
function adapt_sidebar_position() {
|
||||
$bs_sidebar.css({
|
||||
position: "",
|
||||
width: "",
|
||||
});
|
||||
if (config.device.size_class >= config.device.SIZES.LG) {
|
||||
$bs_sidebar.css({
|
||||
position: "fixed",
|
||||
width: $bs_sidebar.outerWidth(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$bs_sidebar.affix({
|
||||
offset: {
|
||||
top: 0,
|
||||
bottom: $('body').height() - $('#wrapwrap').outerHeight() + $("footer").outerHeight(),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
.o_website_quote{
|
||||
h1[id],h2[id]{
|
||||
margin-top: 0px;
|
||||
padding-top: $o-navbar-height;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.website.quote</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='sale_config_website_quote']" position="after">
|
||||
<div class="content-group" attrs="{'invisible': [('module_website_quote','=',False)]}">
|
||||
<div class="mt16">
|
||||
<label for="default_template_id" class="o_light_label"/>
|
||||
<field name="default_template_id" class="oe_inline"/>
|
||||
</div>
|
||||
<div>
|
||||
<button name="%(website_quote.action_sale_quotation_template)d" icon="fa-arrow-right" type="action" string="Quotation Templates" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,167 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_sale_quote_template_search" model="ir.ui.view">
|
||||
<field name="name">sale.quote.template.search</field>
|
||||
<field name="model">sale.quote.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Quotation Template">
|
||||
<filter string="Archived" name="inactive" domain="[('active','=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="view_sale_quote_template_form">
|
||||
<field name="name">sale.quote.template.form</field>
|
||||
<field name="model">sale.quote.template</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale Quotation Template">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
<button string="Edit Template" type="object" name="open_template" class="oe_stat_button" icon="fa-pencil"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Quotation Template"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<label for="number_of_days" string="Quotation expires after"/>
|
||||
<div id="number_of_days">
|
||||
<field name="number_of_days" class="oe_inline"/> days
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Lines">
|
||||
<field name="quote_line" widget="section_and_note_one2many">
|
||||
<form string="Sales Quotation Template Lines">
|
||||
<!--
|
||||
We need the sequence field to be here for new lines to be added at the correct position.
|
||||
TODO: at some point we want to fix this in the framework so that an invisible field is not required.
|
||||
-->
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="display_type" invisible="1"/>
|
||||
<group attrs="{'invisible': [('display_type', '!=', False)]}">
|
||||
<group>
|
||||
<field
|
||||
name="product_id"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)]
|
||||
}"
|
||||
/>
|
||||
<label for="product_uom_qty"/>
|
||||
<div>
|
||||
<field
|
||||
name="product_uom_qty" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<label for="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<div groups="sale.group_discount_per_so_line">
|
||||
<field name="discount" class="oe_inline"/> %%
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook colspan="4">
|
||||
<page string="Description" attrs="{'invisible': [('display_type', '!=', False)]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
<page string="Section" attrs="{'invisible': [('display_type', '!=', 'line_section')]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
<page string="Note" attrs="{'invisible': [('display_type', '!=', 'line_note')]}">
|
||||
<field name="name" />
|
||||
</page>
|
||||
<page string="Website Description">
|
||||
<field name="website_description" />
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<control>
|
||||
<create string="Add a product"/>
|
||||
<create string="Add a section" context="{'default_display_type': 'line_section'}"/>
|
||||
<create string="Add a note" context="{'default_display_type': 'line_note'}"/>
|
||||
</control>
|
||||
|
||||
<field name="display_type" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name" widget="section_and_note_text"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field
|
||||
name="product_uom_id"
|
||||
groups="uom.group_uom"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)]
|
||||
}"
|
||||
/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Suggested Products">
|
||||
<field name="options">
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="uom.group_uom"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Confirmation">
|
||||
<group>
|
||||
<field name="require_signature"/>
|
||||
<field name="require_payment"/>
|
||||
<field name="mail_template_id"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="website_description" invisible="1"/>
|
||||
<field name="note" nolabel="1"
|
||||
placeholder="The Administrator can set default Terms & Conditions in Sales Settings. Terms set here will show up instead if you select this quotation template."/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_sale_quote_template_tree" model="ir.ui.view">
|
||||
<field name="name">sale.quote.template.tree</field>
|
||||
<field name="model">sale.quote.template</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Sale Quotation Template">
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_sale_quotation_template" model="ir.actions.act_window">
|
||||
<field name="name">Quotation Templates</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.quote.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your quotation template
|
||||
</p><p>
|
||||
Use templates to create polished, professional quotes in minutes.
|
||||
Send these quotes by email and let your customers sign online.
|
||||
Use cross-selling and discounts to push and boost your sales.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_sale_quote_template" action="action_sale_quotation_template" parent="sale.menu_sales_config" sequence="1" name="Quotation Templates" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
|
||||
|
||||
<menuitem id='payment_acquirer_menu_sale' action='payment.action_payment_acquirer' parent='sale.menu_sales_config' sequence='3' name='Payment Acquirers' groups="sales_team.group_sale_salesman"/>
|
||||
</odoo>
|
||||
@@ -1,745 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website Quote frontend assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_quote/static/src/js/website_quotation.js"></script>
|
||||
<link rel='stylesheet' href='/website_quote/static/src/css/website_quotation.css'/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_editor" inherit_id="web_editor.assets_editor" name="Website Quote Editor assets">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" type="text/scss" href="/website_quote/static/src/scss/website_quotation.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="pricing" name="Price">
|
||||
<section id="quote">
|
||||
<h1 class="page-header">Pricing</h1>
|
||||
|
||||
<t t-set="display_taxamount" t-value="any([line.tax_id for line in quotation.order_line])"/>
|
||||
<t t-set="display_discount" t-value="True in [line.discount > 0 for line in quotation.order_line]"/>
|
||||
|
||||
<table class="table wq-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<t t-set="colspan" t-value="5"/>
|
||||
<th>Products</th>
|
||||
<th>Quantity</th>
|
||||
<th t-if="display_taxamount">
|
||||
<span>Taxes</span>
|
||||
<t t-set="colspan" t-value="colspan+1"/>
|
||||
</th>
|
||||
<th t-if="display_discount">
|
||||
<span>Discount</span>
|
||||
<t t-set="colspan" t-value="colspan+1"/>
|
||||
</th>
|
||||
<th class="text-right">Unit Price</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>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
|
||||
<t t-foreach="quotation.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-attf-class="{{ 'o_is_' + line.display_type if line.display_type else '' }}">
|
||||
<t t-if="not line.display_type">
|
||||
<td><span t-field="line.name"/></td>
|
||||
<td>
|
||||
<div id="quote_qty">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
<span t-field="line.product_uom" groups="uom.group_uom"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="display_taxamount">
|
||||
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
|
||||
</td>
|
||||
<td t-if="display_discount">
|
||||
<strong t-if="line.discount" class="text-info">
|
||||
<t t-esc="((line.discount % 1) and '%s' or '%d') % line.discount"/>% discount
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div
|
||||
t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
t-att-style="line.discount and 'text-decoration: line-through' or None"
|
||||
t-att-class="(line.discount and 'text-danger' or '') + ' text-right'"
|
||||
/>
|
||||
<div t-if="line.discount">
|
||||
<t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<span
|
||||
t-field="line.price_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&unlink=True&token=#{ quotation.access_token }" class="mb8 js_update_line_json d-print-none" t-if="line.option_line_id" aria-label="Remove" title="Remove">
|
||||
<span class="fa fa-trash-o"></span>
|
||||
</a>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td t-att-colspan="colspan">
|
||||
<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 t-att-colspan="colspan">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-if="current_section and (line_last or quotation.order_line[line_index+1].display_type == 'line_section')">
|
||||
<tr class="is-subtotal text-right">
|
||||
<td t-att-colspan="colspan">
|
||||
<strong>Subtotal:</strong> <span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="clearfix">
|
||||
<div id="total" class="row" name="total">
|
||||
<div t-attf-class="col-md-4 float-right #{'col-4' if not type_html else ''}">
|
||||
<table class="table table-sm" style="min-width: 250px;max-width: 400px;">
|
||||
<tr class="border-black" style="border-bottom:1px solid #dddddd;">
|
||||
<td><strong>Subtotal</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
data-id="total_amount"
|
||||
t-field="quotation.amount_untaxed"
|
||||
t-options='{"widget": "monetary","display_currency": quotation.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="quotation.amount_by_group" t-as="amount_by_group">
|
||||
<tr style="border-bottom:1px solid #dddddd;">
|
||||
<t t-if="amount_by_group[3] == 1 and quotation.amount_untaxed == amount_by_group[2]">
|
||||
<td>
|
||||
<span t-esc="amount_by_group[0]"/>
|
||||
<span>&nbsp;<span>on</span>&nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="amount_by_group[1]"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.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": quotation.pricelist_id.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="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="terms" class="container" t-if="quotation.note">
|
||||
<h2 class="page-header">Terms & Conditions</h2>
|
||||
<p t-field="quotation.note"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="signature" name="Signature">
|
||||
<div t-if="quotation.signature" class="row mt64" name="signature">
|
||||
<div class="offset-8 col-4">
|
||||
<strong>Signature</strong>
|
||||
</div>
|
||||
<div class="offset-8 col-4">
|
||||
<img t-att-src="'data:image/png;base64,%s' % to_text(quotation.signature)" style="max-height: 4cm; max-width: 8cm;"/>
|
||||
</div>
|
||||
<div class="offset-8 col-4 text-center">
|
||||
<p t-field="quotation.signed_by"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="change_quantity" inherit_id="website_quote.pricing" active="False" customize_show="True" name="Change Quantity">
|
||||
<xpath expr="//div[@id='quote_qty']" position="replace">
|
||||
<div class="input-group oe_website_spinner">
|
||||
<div class="input-group-prepend d-print-none">
|
||||
<span class="input-group-text">
|
||||
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&remove=True&token=#{ quotation.access_token }" class="mb8 js_update_line_json" aria-label="Remove one" title="Remove one">
|
||||
<span class="fa fa-minus"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="js_quantity form-control" t-att-data-id="line.id" t-att-value="line.product_uom_qty"/>
|
||||
<div class="input-group-append d-print-none">
|
||||
<span class="input-group-text">
|
||||
<a t-attf-href="./update_line/#{ line.id }/?order_id=#{ quotation.id }&token=#{ quotation.access_token }" class="mb8 js_update_line_json" aria-label="Add one" title="Add one">
|
||||
<span class="fa fa-plus"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- duplicata of the template payment.payment_confirmation_status.
|
||||
The duplication avoid to break an existing installation in stable version-->
|
||||
<template id="payment_confirmation_status">
|
||||
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
|
||||
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
|
||||
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Your payment has been authorized.
|
||||
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Complete page of the quotation -->
|
||||
<template id="so_quotation" name="Product Quotation">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="sales_order" t-value="quotation"/> <!-- rename to ensure breadcrumb compatibility with website_portal_sale's portal_layout inheritance -->
|
||||
<body data-spy="scroll" data-target=".navspy" data-offset="50">
|
||||
<div class="container o_website_quote">
|
||||
<div class="row mt16">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-3 d-print-none">
|
||||
<div class="bs-sidebar">
|
||||
<div class="text-center" t-if="quotation.state in ('draft', 'sent', 'waiting_date')" style="padding: 10px">
|
||||
<a role="button" t-if="order_valid and not quotation.signature and quotation.has_to_be_signed()" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
|
||||
<i class="fa fa-check"/> Accept Order
|
||||
</a>
|
||||
<a role="button" t-elif="order_valid and quotation.has_to_be_paid()" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
|
||||
<i class="fa fa-check"/> <t t-if="not quotation.signature">Accept & </t>Pay
|
||||
</a>
|
||||
<a role="button" t-if="not order_valid" href="#discussion" class="btn btn-info btn-block o_normalwhitespace">
|
||||
<strong>This offer expired!</strong><br/>
|
||||
Contact us for new quote.
|
||||
</a>
|
||||
<div class="mt8" t-if="order_valid">
|
||||
<a data-toggle="modal" data-target="#modaldecline">
|
||||
Reject
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
|
||||
<a role="button" class="btn btn-primary btn-block" data-toggle="modal" data-target="#modalaccept">
|
||||
<i class="fa fa-check"></i> Pay Now
|
||||
</a>
|
||||
</div>
|
||||
<hr class="mt0 mb0"/>
|
||||
<t t-call="website_quote.navigation_menu"/>
|
||||
<hr class="mt0 mb0"/>
|
||||
<div t-if="order_valid and quotation.state in ['draft', 'sent']" class="text-center">
|
||||
<input type="hidden" t-att-value="quotation.validity_date" id="validity_date"/>
|
||||
<div class="mt8" t-if="days_valid > 0">
|
||||
<strong>This offer expires in</strong>
|
||||
<div class="day_counter mt8 mb16">
|
||||
<i class="fa fa-clock-o" aria-title="Dates" title="Dates"></i>
|
||||
<t t-esc="days_valid"/>
|
||||
<t t-if="days_valid > 1">
|
||||
days
|
||||
</t>
|
||||
<t t-if="days_valid <= 1">
|
||||
day
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-center mb16" t-if="quotation.amount_undiscounted > quotation.amount_untaxed">
|
||||
<p class="text-muted mb8">Your advantage:</p>
|
||||
<t t-if="quotation.amount_untaxed == quotation.amount_total">
|
||||
<strong t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<strong t-field="quotation.amount_undiscounted"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
style="text-decoration: line-through"
|
||||
class="text-danger"/>
|
||||
</t>
|
||||
<t t-if="quotation.amount_untaxed != quotation.amount_total">
|
||||
<strong t-field="quotation.amount_untaxed"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<strong t-field="quotation.amount_undiscounted"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
style="text-decoration: line-through"
|
||||
class="text-danger"/>
|
||||
<br />
|
||||
(<span t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/> Incl. tax)
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page content -->
|
||||
<div id="quote_content" class="col-lg-9">
|
||||
|
||||
<div class="alert alert-success alert-dismissable" t-if="message==1" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Your message has been successfully sent!
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==2" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
This quotation has been rejected. <a href="#discussion">Contact us</a> if you want a new one.
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==3" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
This order has been validated. Thanks for your trust
|
||||
and do not hesitate to <a href="#discussion">contact us</a> for
|
||||
any question.
|
||||
</div>
|
||||
<t t-if="payment_tx">
|
||||
<t t-set="payment_tx_id" t-value="payment_tx"/>
|
||||
<t t-call="website_quote.payment_confirmation_status"/>
|
||||
</t>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==4 and quotation.state != 'sent'" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
This order
|
||||
<t t-if="quotation.state=='draft'">has not yet been sent!</t>
|
||||
<t t-if="quotation.state=='cancel'">has already been cancelled!</t>
|
||||
<t t-if="quotation.state not in ('cancel','draft')">has already been validated!</t>
|
||||
You can <a href="#discussion">contact us</a> for
|
||||
any questions.
|
||||
</div>
|
||||
|
||||
<!-- modal relative to the actions Accept/Reject/Cancel -->
|
||||
<div role="dialog" class="modal fade" id="modalaccept">
|
||||
<div class="modal-dialog" t-if="not quotation.signature and quotation.has_to_be_signed()">
|
||||
<form id="accept" method="POST" t-att-data-order-id="quotation.id" t-att-data-token="quotation.access_token" class="js_accept_json modal-content js_website_submit_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Validate Order</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by signing this proposal, I
|
||||
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
|
||||
</p>
|
||||
<t t-call="portal.portal_signature">
|
||||
<t t-set="object" t-value="quotation"/>
|
||||
<t t-set="partner_name" t-value="quotation.partner_id.name"/>
|
||||
<t t-set="callUrl" t-value="'/my/quotes/accept'"/>
|
||||
<t t-set="accessToken" t-value="quotation.access_token"/>
|
||||
</t>
|
||||
</main>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-dialog modal-content" t-if="not (quotation.has_to_be_signed() and not quotation.signature) and (quotation.has_to_be_paid() or need_payment)">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Validate Order</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body" id="sign-dialog">
|
||||
<p>
|
||||
<span>I agree that by paying this proposal, I
|
||||
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
|
||||
<b data-id="total_amount" t-field="quotation.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
|
||||
</p>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
|
||||
<h3 class="mb24">Pay with</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="submit_txt" t-value="'Pay & Confirm'"/>
|
||||
<t t-set="icon_class" t-value="'fa-lock'"/>
|
||||
<t t-set="form_action" t-value="'/quote/' + str(quotation.id) + '/transaction/token'"/>
|
||||
<t t-set="prepare_tx_url" t-value="'/quote/' + str(quotation.id) + '/transaction/'"/>
|
||||
<t t-set="access_token" t-value="quotation.access_token"/>
|
||||
</t>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="dialog" class="modal fade" id="modaldecline">
|
||||
<div class="modal-dialog">
|
||||
<form id="decline" method="POST" t-attf-action="/quote/#{quotation.id}/#{quotation.access_token}/decline" class="modal-content">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Reject This Quote</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<p>
|
||||
Tell us why you are refusing this quotation, this will help us improve our services.
|
||||
</p>
|
||||
<textarea rows="4" name="decline_message" placeholder="Your feedback....." class="form-control"/>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<button type="submit" t-att-id="quotation.id" class="btn btn-primary">Reject</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning alert-dismissable d-print-none" t-if="quotation.state == 'cancel'" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="close">&times;</button>
|
||||
<strong>This quotation has been canceled.</strong> Contact us to get a new quote.
|
||||
</div>
|
||||
|
||||
<t t-call="website_quote.so_quotation_content"/>
|
||||
|
||||
<div class="text-center d-print-none mt16 mb16" id="quotation_toolbar" t-if="quotation.state in ('draft', 'sent', 'waiting_date')">
|
||||
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and not quotation.has_to_be_paid()">
|
||||
<i class="fa fa-check"></i> Accept
|
||||
</a>
|
||||
<a role="button" class="btn btn-success" data-toggle="modal" data-target="#modalaccept" t-if="order_valid and quotation.has_to_be_paid()">
|
||||
<i class="fa fa-check"></i> Accept & Pay
|
||||
</a>
|
||||
<a role="button" class="btn btn-danger" data-toggle="modal" data-target="#modaldecline">
|
||||
<i class="fa fa-times"></i> Reject
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center" t-if="need_payment or (quotation.state in ('sale') and quotation.has_to_be_paid() and tx_state != 'done')" style="padding: 10px">
|
||||
<a role="button" class="btn btn-primary" data-toggle="modal" data-target="#modalaccept">
|
||||
<i class="fa fa-check"></i> Pay Now
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div><!-- //col-lg-9 -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="chatter" name="Allow Comments & History" customize_show="True" inherit_id="website_quote.so_quotation">
|
||||
<xpath expr="//div[@id='quote_content']" position="inside">
|
||||
<h1 class="page-header">History</h1>
|
||||
<!-- Options:Quotation Chatter: user can reply -->
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="quotation"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//a[@data-target='#modaldecline']" position="before">
|
||||
<a type="submit" href="#discussion">
|
||||
Ask Changes
|
||||
</a>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='quotation_toolbar']/a[@data-target='#modaldecline']" position="before">
|
||||
<a role="button" class="btn btn-info" type="submit" href="#discussion">
|
||||
<i class="fa fa-comment"></i> Feedback
|
||||
</a>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Quotation content : intro, informations, order lines, remarks, descriptions ....
|
||||
This template should contains all the printable element of the SO. This is the
|
||||
template rendered in PDF with the report engine.
|
||||
-->
|
||||
<template id="so_quotation_content" name="Quotation Content">
|
||||
<!-- Intro -->
|
||||
<div id="introduction" class="page-header mt16">
|
||||
<h1>
|
||||
<t t-if="quotation.state in ('draft','sent','cancel')">Your Quotation</t>
|
||||
<t t-if="quotation.state not in ('draft','sent','cancel')">Your Order</t>
|
||||
<em t-esc="quotation.name"/>
|
||||
<small t-field="quotation.state"/>
|
||||
<div t-ignore="true" class="float-right css_editable_mode_hidden d-print-none">
|
||||
<ul class="list-inline">
|
||||
<li groups="sales_team.group_sale_salesman" class="list-inline-item">
|
||||
<a role="button" t-if="quotation.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Edit Quote</a>
|
||||
<a role="button" t-if="quotation.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Back to Sales Order</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a role="button" class="btn btn-info" target="_blank" t-att-href="'/quote/%s/%s' % (quotation.id,quotation.access_token)+'?pdf=True'">Print</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Informations -->
|
||||
<div class="row mt32">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="row">
|
||||
<label class="col-md-4 col-4 text-right">Customer:</label>
|
||||
<div class="col-md-8 col-8">
|
||||
<div t-field="quotation.partner_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-md-4 col-4 text-right">Bill To:</label>
|
||||
<div class="col-md-8 col-8">
|
||||
<div t-field="quotation.partner_invoice_id" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["address", "name", "phone", "email"]
|
||||
}'/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="quotation.partner_shipping_id.id != quotation.partner_invoice_id.id" class="row">
|
||||
<label class="col-md-4 col-4 text-right">Ship To:</label>
|
||||
<div class="col-md-8 col-8">
|
||||
<div t-field="quotation.partner_shipping_id" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["address", "name", "phone"]
|
||||
}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="row">
|
||||
<label class="col-md-5 col-4 text-right">Your Contact:</label>
|
||||
<div class="col-md-7 col-8">
|
||||
<div t-field="quotation.user_id" t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-md-5 col-4 text-right">Quote Date:</label>
|
||||
<div class="col-md-7 col-8">
|
||||
<span t-field="quotation.date_order"/>
|
||||
</div>
|
||||
<div class="clearfix"/>
|
||||
<div t-if="quotation.validity_date and quotation.state in ['draft', 'sent']">
|
||||
<label class="col-md-5 col-4 text-right">Expiration Date:</label>
|
||||
<div class="col-md-7 col-8">
|
||||
<span t-field="quotation.validity_date"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"/>
|
||||
<div t-if="quotation.client_order_ref">
|
||||
<label class="col-md-5 col-4 text-right">Your Reference:</label>
|
||||
<div class="col-md-7 col-8">
|
||||
<span t-field="quotation.client_order_ref"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"/>
|
||||
<div t-if="quotation.payment_term_id">
|
||||
<label class="col-md-5 col-4 text-right">Payment Terms:</label>
|
||||
<div class="col-md-7 col-8">
|
||||
<span t-field="quotation.payment_term_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Descriptions -->
|
||||
<div t-field="quotation.website_description" class="oe_no_empty"/>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="quotation.order_line" t-as="line">
|
||||
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
|
||||
<a t-att-id="line.id"/>
|
||||
<div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/>
|
||||
</t>
|
||||
</t>
|
||||
<div class="oe_structure"/>
|
||||
<!-- Pricing -->
|
||||
<t t-call="website_quote.pricing"/>
|
||||
<!-- Signature -->
|
||||
<t t-call="website_quote.signature"/>
|
||||
<!-- Options -->
|
||||
<t t-call="website_quote.optional_products"/>
|
||||
</template>
|
||||
|
||||
|
||||
<template id="navigation_menu">
|
||||
<div class="navspy" t-ignore="true" role="complementary">
|
||||
<ul class="nav bs-sidenav" data-id="quote_sidebar">
|
||||
<li class="nav-item"><a href="#introduction" class="nav-link">Introduction</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="optional_products">
|
||||
<t t-if="any([(not option.line_id) for option in quotation.options])">
|
||||
<section>
|
||||
<h1 class="page-header">Suggested Products</h1>
|
||||
</section>
|
||||
<section>
|
||||
<table class="table table-hover wq-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th></th>
|
||||
<th class="text-right">Price</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="quotation.options" t-as="option">
|
||||
<t t-if="not option.line_id">
|
||||
<td>
|
||||
<div t-field="option.name"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-if="option.discount" class="text-info">
|
||||
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong class="text-right">
|
||||
<div t-field="option.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'
|
||||
t-att-style="option.discount and 'text-decoration: line-through' or None"
|
||||
t-att-class="option.discount and 'text-danger' or None"/>
|
||||
<div t-if="option.discount">
|
||||
<t t-esc="(1-option.discount / 100.0) * option.price_unit" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
|
||||
</div>
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-center" t-if="quotation.state in ['draft', 'sent']">
|
||||
<a t-attf-href="/quote/add_line/#{ option.id }/#{ quotation.id }/#{ quotation.access_token }" class="mb8 d-print-none" aria-label="Add to cart" title="Add to cart">
|
||||
<span class="fa fa-shopping-cart"/>
|
||||
</a>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Template to edit the quotation template with the website editor -->
|
||||
<template id="so_template" name="SO Template">
|
||||
<t t-call="website.layout">
|
||||
<body data-spy="scroll" data-target=".navspy" data-offset="50">
|
||||
<div class="container o_website_quote">
|
||||
<div class="row mt16">
|
||||
<div class="col-lg-3 d-print-none">
|
||||
<div class="bs-sidebar">
|
||||
<div class="navspy" role="complementary" t-ignore="True">
|
||||
<ul class="nav bs-sidenav" data-id="quote_sidebar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="row mb8">
|
||||
<div t-ignore="true" class="css_editable_mode_hidden offset-lg-11">
|
||||
<a role="button" class="btn btn-info d-print-none" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (template._name, template.id, request.env.ref('website_quote.action_sale_quotation_template').id)">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info alert-dismissable" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
<p>
|
||||
<strong>Template Header:</strong> this content
|
||||
will appear on all quotations using this
|
||||
template.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
Titles with style <i>Heading 1</i> and
|
||||
<i>Heading 2</i> will be used to generate the
|
||||
table of content automatically.
|
||||
</p>
|
||||
</div>
|
||||
<div id="template_introduction" t-field="template.website_description" class="oe_no_empty"/>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="template.quote_line" t-as="line">
|
||||
<t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/>
|
||||
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Product: <strong t-esc="line.product_id.name"/>:
|
||||
this content will appear on the quotation only if this
|
||||
product is put on the quote.
|
||||
</div>
|
||||
<div t-field="line.website_description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="product_tmpl_ids" t-value="[]"/>
|
||||
<t t-foreach="template.options" t-as="option_line">
|
||||
<t t-if="option_line.product_id.product_tmpl_id.id not in product_tmpl_ids">
|
||||
<t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [option_line.product_id.product_tmpl_id.id]"/>
|
||||
<div class="alert alert-info alert-dismissable mt16" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Optional Product: <strong t-esc="option_line.product_id.name"/>:
|
||||
this content will appear on the quotation only if this
|
||||
product is used in the quote.
|
||||
</div>
|
||||
<div t-field="option_line.website_description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
<section id="terms" class="container" t-if="template.note">
|
||||
<h1 t-ignore="True">Terms & Conditions</h1>
|
||||
<p t-field="template.note"/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="website_quote.brand_promotion" inherit_id="website.brand_promotion">
|
||||
<xpath expr="//div[hasclass('o_brand_promotion')]" position="replace">
|
||||
<div class="o_brand_promotion">
|
||||
Powered by <a target="_blank" class="badge badge-danger" href="http://www.odoo.com/page/website-builder?utm_source=db&utm_medium=website">Odoo</a>,
|
||||
an awesome <a target="_blank" href="http://www.odoo.com/page/crm?utm_source=db&utm_medium=website">Open Source CRM</a>.
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_quotations_inherit_quote" inherit_id="sale.portal_my_quotations">
|
||||
<xpath expr="//t[@t-foreach='quotations']/tr/td/a" position="replace">
|
||||
<t t-if="quotation.template_id and quotation.template_id.active">
|
||||
<a t-attf-href="/quote/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-attf-href="/my/orders/#{quotation.id}?#{keep_query()}"><t t-esc="quotation.name"/></a>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -6,7 +6,7 @@
|
||||
'website': 'https://www.odoo.com/page/e-commerce',
|
||||
'version': '1.0',
|
||||
'description': "",
|
||||
'depends': ['website', 'sale_payment', 'website_payment', 'website_mail', 'website_form', 'website_rating', 'digest', 'account'],
|
||||
'depends': ['website', 'sale', 'website_payment', 'website_mail', 'website_form', 'website_rating', 'digest'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_sale.xml',
|
||||
|
||||
@@ -6,6 +6,25 @@
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//div[@id='sales_settings_invoicing_policy']" position="after">
|
||||
<!--
|
||||
Show the automatic invoices setting in sales settings.
|
||||
-->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
attrs="{'invisible': ['|', ('default_invoice_policy', '!=', 'order'), ('portal_confirmation_pay', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="automatic_invoice" nolabel="1"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="automatic_invoice"/>
|
||||
<div class="text-muted">
|
||||
Generate the invoice automatically when the payment is received
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<div id="webmaster_settings" position="after">
|
||||
<h2>Product Catalog</h2>
|
||||
<div class="row mt16 o_settings_container" id="sale_product_catalog_settings">
|
||||
|
||||
@@ -1626,8 +1626,8 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_page_products_links" name="Orders Followup Products Links" inherit_id="sale.portal_order_page">
|
||||
<xpath expr="//div[@id='product_name']/*" position="replace">
|
||||
<template id="sale_order_portal_content_inherit_website_sale" name="Orders Followup Products Links" inherit_id="sale.sale_order_portal_content">
|
||||
<xpath expr="//section[@id='details']//td[@id='product_name']/*" position="replace">
|
||||
<a t-if="line.product_id.website_published" t-att-href="line.product_id.website_url">
|
||||
<span t-field="line.name" />
|
||||
</a>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_order_page_shipping_tracking" name="Shipping tracking on orders followup" inherit_id="sale_stock.portal_order_page_shipping">
|
||||
<template id="sale_order_portal_content_inherit_sale_stock_inherit_website_sale_delivery" name="Shipping tracking on orders followup" inherit_id="sale_stock.sale_order_portal_content_inherit_sale_stock">
|
||||
<xpath expr="//div[@id='picking_info']" position="after">
|
||||
<div t-if="i.carrier_tracking_ref">
|
||||
Tracking: <span t-field="i.carrier_id.name"/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="portal_order_page_downloads" name="Orders Downloads Followup" inherit_id="sale.portal_order_page">
|
||||
<xpath expr="//div[@id='product_name']" position="inside">
|
||||
<t t-set="attachments" t-value="digital_attachments.get(line.product_id.id)"/>
|
||||
<template id="sale_order_portal_content_inherit_website_sale_digital" name="Orders Downloads Followup" inherit_id="sale.sale_order_portal_content">
|
||||
<xpath expr="//section[@id='details']//td[@id='product_name']" position="inside">
|
||||
<t t-if="digital_attachments" t-set="attachments" t-value="digital_attachments.get(line.product_id.id)"/>
|
||||
<t t-if="attachments">
|
||||
<span class="dropdown">
|
||||
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown">
|
||||
|
||||