diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index c40d6d47484..3ae15011263 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import SUPERUSER_ID -from openerp import http -from openerp.http import request import werkzeug -import datetime -import time -from openerp.tools.translate import _ -from openerp.addons.website_mail.controllers.main import _message_post_helper +from odoo import fields, http, _ +from odoo.http import request +from odoo.addons.website_mail.controllers.main import _message_post_helper class sale_quote(http.Controller): @@ -18,96 +14,93 @@ class sale_quote(http.Controller): "/quote//" ], type='http', auth="public", website=True) def view(self, order_id, pdf=None, token=None, message=False, **post): - # use SUPERUSER_ID allow to access/view order for public user + # use sudo to allow accessing/viewing orders for public user # only if he knows the private token - now = time.strftime('%Y-%m-%d') + now = fields.Date.today() if token: - order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)]) + Order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)]) # Log only once a day - if order and request.session.get('view_quote', False) != now: + if Order and request.session.get('view_quote') != now: request.session['view_quote'] = now body = _('Quotation viewed by customer') - _message_post_helper(res_model='sale.order', res_id=order.id, message=body, token=token, token_field="access_token", message_type='notification') + _message_post_helper(res_model='sale.order', res_id=Order.id, message=body, token=token, token_field="access_token", message_type='notification') else: - order = request.env['sale.order'].search([('id', '=', order_id)]) + Order = request.env['sale.order'].search([('id', '=', order_id)]) - if not order: + if not Order: return request.render('website.404') - dummy, action = request.env['ir.model.data'].get_object_reference('sale', 'action_quotations') days = 0 - if order.validity_date: - days = (datetime.datetime.strptime(order.validity_date, '%Y-%m-%d') - datetime.datetime.now()).days + 1 + if Order.validity_date: + days = (fields.Date.from_string(Order.validity_date) - fields.Date.from_string(fields.Date.today())).days + 1 if pdf: - report_obj = request.registry['report'] - pdf = report_obj.get_pdf(request.cr, SUPERUSER_ID, [order_id], 'website_quote.report_quote', data=None, context=dict(request.context, set_viewport_size=True)) + pdf = request.env['report'].sudo().with_context(set_viewport_size=True).get_pdf(Order.id, 'website_quote.report_quote') pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) - user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) - tx_id = request.session.get('quote_%s_transaction_id' % order.id) - if not tx_id: - tx_id = request.registry['payment.transaction'].search(request.cr, SUPERUSER_ID, [('reference', '=', order.name)], context=request.context) - tx = request.registry['payment.transaction'].browse(request.cr, SUPERUSER_ID, tx_id, context=request.context) if tx_id else False + transaction_id = request.session.get('quote_%s_transaction_id' % Order.id) + if not transaction_id: + Transaction = request.env['payment.transaction'].sudo().search([('reference', '=', Order.name)]) + else: + Transaction = request.env['payment.transaction'].sudo().browse(transaction_id) values = { - 'quotation': order, + 'quotation': Order, 'message': message and int(message) or False, - 'option': bool(filter(lambda x: not x.line_id, order.options)), - 'order_valid': (not order.validity_date) or (now <= order.validity_date), + 'option': bool(filter(lambda x: not x.line_id, Order.options)), + 'order_valid': (not Order.validity_date) or (now <= Order.validity_date), 'days_valid': days, - 'action': action, - 'breadcrumb': user.partner_id == order.partner_id, - 'tx_id': tx_id, - 'tx_state': tx.state if tx else False, - 'tx_post_msg': tx.acquirer_id.post_msg if tx else False, - 'need_payment': order.invoice_status == 'to invoice' and (not tx or tx.state in ['draft', 'cancel', 'error']), + 'action': request.env.ref('sale.action_quotations').id, + 'breadcrumb': request.env.user.partner_id == Order.partner_id, + 'tx_id': Transaction.id if Transaction else False, + 'tx_state': Transaction.state if Transaction else False, + 'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False, + 'need_payment': Order.invoice_status == 'to invoice' and Transaction.state in ['draft', 'cancel', 'error'], 'token': token, } - if order.require_payment or values['need_payment']: - payment_obj = request.registry.get('payment.acquirer') - acquirer_ids = payment_obj.search(request.cr, SUPERUSER_ID, [('website_published', '=', True), ('company_id', '=', order.company_id.id)], context=request.context) - values['acquirers'] = list(payment_obj.browse(request.cr, token and SUPERUSER_ID or request.uid, acquirer_ids, context=request.context)) - render_ctx = dict(request.context, submit_class='btn btn-primary', submit_txt=_('Pay & Confirm')) + if Order.require_payment or values['need_payment']: + if token: + values['acquirers'] = list(request.env['payment.acquirer'].sudo().search([('website_published', '=', True), ('company_id', '=', Order.company_id.id)])) + else: + values['acquirers'] = list(request.env['payment.acquirer'].sudo(user=request.uid).search([('website_published', '=', True), ('company_id', '=', Order.company_id.id)])) + extra_context = { + 'submit_class': 'btn btn-primary', + 'submit_txt': _('Pay & Confirm') + } for acquirer in values['acquirers']: - acquirer.button = payment_obj.render( - request.cr, SUPERUSER_ID, acquirer.id, + # TOFIX: very ugly/unreliable way to assign property to recordset, recordset added to list for shallow reference + acquirer.button = acquirer.with_context(**extra_context).render( '/', - order.amount_total, - order.pricelist_id.currency_id.id, + Order.amount_total, + Order.pricelist_id.currency_id.id, values={ 'return_url': '/quote/%s/%s' % (order_id, token) if token else '/quote/%s' % order_id, 'type': 'form', 'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'), - 'partner_id': order.partner_id.id, - }, - context=render_ctx) + 'partner_id': Order.partner_id.id, + }) return request.render('website_quote.so_quotation', values) @http.route(['/quote/accept'], type='json', auth="public", website=True) def accept(self, order_id, token=None, signer=None, sign=None, **post): - order_obj = request.registry.get('sale.order') - order = order_obj.browse(request.cr, SUPERUSER_ID, order_id) - if token != order.access_token: + Order = request.env['sale.order'].sudo().browse(order_id) + if token != Order.access_token or Order.require_payment: return request.render('website.404') - if order.require_payment: - return request.render('website.404') - if order.state != 'sent': + if Order.state != 'sent': return False - attachments=sign and [('signature.png', sign.decode('base64'))] or [] - order_obj.action_confirm(request.cr, SUPERUSER_ID, [order_id], context=request.context) + attachments = [('signature.png', sign.decode('base64'))] if sign else [] + Order.action_confirm() message = _('Order signed by %s') % (signer,) _message_post_helper(message=message, res_id=order_id, res_model='sale.order', attachments=attachments, **({'token': token, 'token_field': 'access_token'} if token else {})) return True @http.route(['/quote///decline'], type='http', auth="public", methods=['POST'], website=True) def decline(self, order_id, token, **post): - order_obj = request.registry.get('sale.order') - order = order_obj.browse(request.cr, SUPERUSER_ID, order_id) - if token != order.access_token: + Order = request.env['sale.order'].sudo().browse(order_id) + if token != Order.access_token: return request.render('website.404') - if order.state != 'sent': + if Order.state != 'sent': return werkzeug.utils.redirect("/quote/%s/%s?message=4" % (order_id, token)) - request.registry.get('sale.order').action_cancel(request.cr, SUPERUSER_ID, [order_id]) + 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, 'token_field': 'access_token'} if token else {}) @@ -115,55 +108,49 @@ class sale_quote(http.Controller): @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.registry.get('sale.order').browse(request.cr, SUPERUSER_ID, int(order_id)) - if token != order.access_token: + 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'): + if Order.state not in ('draft', 'sent'): return False - line_id=int(line_id) + OrderLine = request.env['sale.order.line'].sudo().browse(int(line_id)) if unlink: - request.registry.get('sale.order.line').unlink(request.cr, SUPERUSER_ID, [line_id], context=request.context) + OrderLine.unlink() return False - number=(remove and -1 or 1) - - order_line_obj = request.registry.get('sale.order.line') - order_line_val = order_line_obj.read(request.cr, SUPERUSER_ID, [line_id], [], context=request.context)[0] - quantity = order_line_val['product_uom_qty'] + number - order_line_obj.write(request.cr, SUPERUSER_ID, [line_id], {'product_uom_qty': (quantity)}, context=request.context) - return [str(quantity), str(order.amount_total)] + 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/"], type='http', auth="user", website=True) def template_view(self, quote, **post): - values = { 'template': quote } + values = {'template': quote} return request.render('website_quote.so_template', values) @http.route(["/quote/add_line///"], type='http', auth="public", website=True) def add(self, option_id, order_id, token, **post): - vals = {} - order = request.registry.get('sale.order').browse(request.cr, SUPERUSER_ID, order_id) - if token != order.access_token: + 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']: + 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_obj = request.registry.get('sale.order.option') - option = option_obj.browse(request.cr, SUPERUSER_ID, option_id) - + 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, - 'layout_category_id': option.layout_category_id.id, - 'product_uom_qty': option.quantity, - 'product_uom': option.uom_id.id, - 'discount': option.discount, + 'price_unit': Option.price_unit, + 'website_description': Option.website_description, + 'name': Option.name, + 'order_id': Order.id, + 'product_id': Option.product_id.id, + 'layout_category_id': Option.layout_category_id.id, + 'product_uom_qty': Option.quantity, + 'product_uom': Option.uom_id.id, + 'discount': Option.discount, } - line = request.registry.get('sale.order.line').create(request.cr, SUPERUSER_ID, vals, context=request.context) - request.registry.get('sale.order.line')._compute_tax_id(request.cr, SUPERUSER_ID, [line], context=request.context) - option_obj.write(request.cr, SUPERUSER_ID, [option.id], {'line_id': line}, context=request.context) - return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (order.id, token)) + OrderLine = request.env['sale.order.line'].sudo().create(vals) + OrderLine._compute_tax_id() + Option.write({'line_id': OrderLine}) + return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (Order.id, token)) # note dbo: website_sale code @http.route(['/quote//transaction/'], type='json', auth="public", website=True) @@ -180,55 +167,50 @@ class sale_quote(http.Controller): :param int acquirer_id: id of a payment.acquirer record. If not set the user is redirected to the checkout page """ - cr, uid, context = request.cr, request.uid, request.context - payment_obj = request.registry.get('payment.acquirer') - transaction_obj = request.registry.get('payment.transaction') - order = request.registry.get('sale.order').browse(cr, SUPERUSER_ID, order_id, context=context) + PaymentTransaction = request.env['payment.transaction'].sudo() - if not order or not order.order_line or acquirer_id is None: - return request.redirect("/quote/" + str(order_id)) + Order = request.env['sale.order'].sudo().browse(order_id) + if not Order or not Order.order_line or acquirer_id is None: + return request.redirect("/quote/%s" % order_id) # find an already existing transaction - tx_id = transaction_obj.search(cr, SUPERUSER_ID, [('reference', '=', order.name)], context=context) - tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) - if tx: - if tx.sale_order_id.id != order.id or tx.state in ['error', 'cancel'] or tx.acquirer_id.id != acquirer_id: - tx = False - tx_id = False - elif tx.state == 'draft': - tx.write({ - 'amount': order.amount_total, + Transaction = PaymentTransaction.search([('reference', '=', Order.name)]) + if Transaction: + if Transaction.sale_order_id != Order or Transaction.state in ['error', 'cancel'] or Transaction.acquirer_id.id != acquirer_id: + Transaction = False + elif Transaction.state == 'draft': + Transaction.write({ + 'amount': Order.amount_total, }) - if not tx: - tx_id = transaction_obj.create(cr, SUPERUSER_ID, { + if not Transaction: + Transaction = PaymentTransaction.create({ 'acquirer_id': acquirer_id, - 'type': order._get_payment_type(), - 'amount': order.amount_total, - 'currency_id': order.pricelist_id.currency_id.id, - 'partner_id': order.partner_id.id, - 'reference': transaction_obj.get_next_reference(cr, uid, order.name, context=context), - 'sale_order_id': order.id, - 'callback_eval': "self.env['sale.order']._confirm_online_quote(self.sale_order_id.id, self)" - }, context=context) - request.session['quote_%s_transaction_id' % order.id] = tx_id - tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) - # update quotation - request.registry['sale.order'].write( - cr, SUPERUSER_ID, [order.id], { - 'payment_acquirer_id': acquirer_id, - 'payment_tx_id': tx_id - }, context=context) + 'type': Order._get_payment_type(), + 'amount': Order.amount_total, + 'currency_id': Order.pricelist_id.currency_id.id, + 'partner_id': Order.partner_id.id, + 'reference': PaymentTransaction.get_next_reference(Order.name), + 'sale_order_id': Order.id, + 'callback_eval': "self.sale_order_id._confirm_online_quote(self)" + }) + request.session['quote_%s_transaction_id' % Order.id] = Transaction.id - return payment_obj.render( - request.cr, SUPERUSER_ID, tx.acquirer_id.id, - tx.reference, - order.amount_total, - order.pricelist_id.currency_id.id, + # update quotation + Order.write({ + 'payment_acquirer_id': acquirer_id, + 'payment_tx_id': Transaction.id + }) + + return Transaction.acquirer_id.with_context( + submit_class='btn btn-primary', + submit_txt=_('Pay & Confirm')).render( + Transaction.reference, + Order.amount_total, + Order.pricelist_id.currency_id.id, values={ 'return_url': '/quote/%s/%s' % (order_id, token) if token else '/quote/%s' % order_id, - 'type': order._get_payment_type(), + 'type': Order._get_payment_type(), 'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'), - 'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id, - 'billing_partner_id': order.partner_invoice_id.id, - }, - context=dict(context, submit_class='btn btn-primary', submit_txt=_('Pay & Confirm'))) + 'partner_id': Order.partner_shipping_id.id or Order.partner_invoice_id.id, + 'billing_partner_id': Order.partner_invoice_id.id, + }) diff --git a/addons/website_quote/data/website_quotation_data.xml b/addons/website_quote/data/website_quotation_data.xml index 1e28def4549..8e0c4a73e6f 100644 --- a/addons/website_quote/data/website_quotation_data.xml +++ b/addons/website_quote/data/website_quotation_data.xml @@ -1,4 +1,5 @@ - + + @@ -148,4 +149,4 @@ - + diff --git a/addons/website_quote/data/website_quotation_demo.xml b/addons/website_quote/data/website_quotation_demo.xml index 41ecd810a9f..90973a1ae50 100644 --- a/addons/website_quote/data/website_quotation_demo.xml +++ b/addons/website_quote/data/website_quotation_demo.xml @@ -1,6 +1,5 @@ - - + Functional Training @@ -523,6 +522,4 @@ - - - + diff --git a/addons/website_quote/models/product_template.py b/addons/website_quote/models/product_template.py index 96f4ad4d0b4..ef2773225f1 100644 --- a/addons/website_quote/models/product_template.py +++ b/addons/website_quote/models/product_template.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import osv, fields +from odoo import fields, models -class product_template(osv.Model): + +class ProductTemplate(models.Model): _inherit = "product.template" - _columns = { - 'website_description': fields.html('Description for the website'), # hack, if website_sale is not installed - 'quote_description': fields.html('Description for the quote'), - } + website_description = fields.Html('Description for the website', sanitize=False) # hack, if website_sale is not installed + quote_description = fields.Html('Description for the quote', sanitize=False) diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index 93b8bc922c6..784b89ecd3e 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -1,119 +1,93 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api -from openerp.osv import osv, fields import uuid -import time -import datetime +from datetime import datetime, timedelta -import openerp.addons.decimal_precision as dp -from openerp import SUPERUSER_ID -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.tools.translate import _, html_translate +from odoo import api, fields, models, _ +from odoo.tools.translate import html_translate +import odoo.addons.decimal_precision as dp -class sale_order_line(osv.osv): +class SaleOrderLine(models.Model): _inherit = "sale.order.line" _description = "Sales Order Line" - _columns = { - 'website_description': fields.html('Line Description', sanitize=False, translate=html_translate), - 'option_line_id': fields.one2many('sale.order.option', 'line_id', 'Optional Products Lines'), - } - def _inject_quote_description(self, cr, uid, values, context=None): + 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.order_id.template_id: + self.name = next((quote_line.name for quote_line in self.order_id.template_id.quote_line if + quote_line.product_id.id == self.product_id.id), self.name) + 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.pool['product.product'].browse(cr, uid, values['product_id'], context=context) + product = self.env['product.product'].browse(values['product_id']) values['website_description'] = product.quote_description or product.website_description return values - def create(self, cr, uid, values, context=None): - values = self._inject_quote_description(cr, uid, values, context) - ret = super(sale_order_line, self).create(cr, uid, values, context=context) - # hack because create don t make the job for a related field - if values.get('website_description'): - self.write(cr, uid, ret, {'website_description': values['website_description']}, context=context) - return ret - def write(self, cr, uid, ids, values, context=None): - values = self._inject_quote_description(cr, uid, values, context) - return super(sale_order_line, self).write(cr, uid, ids, values, context=context) - - # The overrided onchange was written in new api, sorry - # Take the description on the order template if the product is present in it - @api.multi - @api.onchange('product_id') - def product_id_change(self): - domain = super(sale_order_line, self).product_id_change() - if self.order_id.template_id: - self.name = next((quote_line.name for quote_line in self.order_id.template_id.quote_line if quote_line.product_id.id == self.product_id.id), self.name) - return domain - - -class sale_order(osv.osv): +class SaleOrder(models.Model): _inherit = 'sale.order' - def _get_total(self, cr, uid, ids, name, arg, context=None): - res = {} - for order in self.browse(cr, uid, ids, context=context): - total = 0.0 - for line in order.order_line: - total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty - res[order.id] = total - return res + def _get_default_template_id(self): + return self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False) - _columns = { - 'access_token': fields.char('Security Token', required=True, copy=False), - 'template_id': fields.many2one('sale.quote.template', 'Quotation Template', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), - 'website_description': fields.html('Description', translate=html_translate, sanitize=False), - 'options' : fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True), - 'amount_undiscounted': fields.function(_get_total, string='Amount Before Discount', type="float", digits=0), - 'quote_viewed': fields.boolean('Quotation Viewed'), - 'require_payment': fields.selection([ - (0, 'Not mandatory on website quote validation'), - (1, 'Immediate after website order validation'), - (2, 'Immediate after website order validation and save a token'), - ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote"), - } + access_token = fields.Char( + 'Security Token', copy=False, default=lambda self: str(uuid.uuid4()), + required=True) + template_id = fields.Many2one( + 'sale.quote.template', 'Quotation Template', + default=_get_default_template_id, readonly=True, + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) + website_description = fields.Html('Description', sanitize=False, translate=html_translate) + options = 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_payment = fields.Selection([ + (0, 'Not mandatory on website quote validation'), + (1, 'Immediate after website order validation'), + (2, 'Immediate after website order validation and save a token'), + ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote") - def _get_template_id(self, cr, uid, context=None): - try: - template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'website_quote', 'website_quote_template_default')[1] - except ValueError: - template_id = False - return template_id + @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 - _defaults = { - 'access_token': lambda self, cr, uid, ctx={}: str(uuid.uuid4()), - 'template_id': _get_template_id, - } + @api.onchange('template_id') + def onchange_template_id(self): + if not self.template_id: + return + if self.partner_id: + self = self.with_context(lang=self.partner_id.lang) - def open_quotation(self, cr, uid, quote_id, context=None): - quote = self.browse(cr, uid, quote_id[0], context=context) - self.write(cr, uid, quote_id[0], {'quote_viewed': True}, context=context) - return { - 'type': 'ir.actions.act_url', - 'target': 'self', - 'url': '/quote/%s/%s' % (quote.id, quote.access_token) - } - - def onchange_template_id(self, cr, uid, ids, template_id, partner=False, fiscal_position_id=False, pricelist_id=False, context=None): - if not template_id: - return {} - - if partner: - context = dict(context or {}) - context['lang'] = self.pool['res.partner'].browse(cr, uid, partner, context=context).lang - - pricelist_obj = self.pool['product.pricelist'] - - lines = [(5,)] - quote_template = self.pool.get('sale.quote.template').browse(cr, uid, template_id, context=context) - for line in quote_template.quote_line: - if pricelist_id: - uom_context = dict(context, uom=line.product_uom_id.id) - price = pricelist_obj.get_product_price(cr, uid, [pricelist_id], line.product_id, 1, False, context=uom_context) + order_lines = [(5, 0, 0)] + for line in self.template_id.quote_line: + if self.pricelist_id: + price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False) else: price = line.price_unit @@ -127,27 +101,20 @@ class sale_order(osv.osv): 'product_uom': line.product_uom_id.id, 'website_description': line.website_description, 'state': 'draft', - 'customer_lead': self._get_customer_lead(cr, uid, line.product_id.product_tmpl_id), + 'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id), } + order_lines.append((0, 0, data)) - if fiscal_position_id: - fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position_id) - taxes = fpos.map_tax(line.product_id.product_tmpl_id.taxes_id).ids - else: - taxes = line.product_id.product_tmpl_id.taxes_id.ids - data['tax_id'] = [(6, 0, taxes)] + self.order_line = order_lines + self.order_line._compute_tax_id() - lines.append((0, 0, data)) - - options = [] - for option in quote_template.options: - if pricelist_id: - uom_context = context.copy() - uom_context['uom'] = option.uom_id.id - price = pricelist_obj.get_product_price(cr, uid, [pricelist_id], option.product_id, 1, False, context=uom_context) + option_lines = [] + for option in self.template_id.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 - options.append((0, 0, { + data = { 'product_id': option.product_id.id, 'layout_category_id': option.layout_category_id, 'name': option.name, @@ -156,131 +123,97 @@ class sale_order(osv.osv): 'price_unit': price, 'discount': option.discount, 'website_description': option.website_description, - })) - date = False - if quote_template.number_of_days > 0: - date = (datetime.datetime.now() + datetime.timedelta(quote_template.number_of_days)).strftime(DEFAULT_SERVER_DATE_FORMAT) - data = { - 'order_line': lines, - 'website_description': quote_template.website_description, - 'options': options, - 'validity_date': date, - 'require_payment': quote_template.require_payment - } - if quote_template.note: - data['note'] = quote_template.note - return {'value': data} + } + option_lines.append((0, 0, data)) + self.options = option_lines + if self.template_id.number_of_days > 0: + self.validity_date = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days)) + + self.website_description = self.template_id.website_description + self.require_payment = self.template_id.require_payment + + if self.template_id.note: + self.note = self.template_id.note + + @api.multi + def open_quotation(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) + } + + # no call to this method ??? def recommended_products(self, cr, uid, ids, context=None): order_line = self.browse(cr, uid, ids[0], context=context).order_line - product_pool = self.pool.get('product.product') + # product_pool = self.pool.get('product.product') products = [] for line in order_line: products += line.product_id.product_tmpl_id.recommended_products(context=context) return products - def get_access_action(self, cr, uid, ids, context=None): + @api.multi + def get_access_action(self): """ Override method that generated the link to access the document. Instead of the classic form view, redirect to the online quote if exists. """ - quote = self.browse(cr, uid, ids[0], context=context) - if not quote.template_id: - return super(sale_order, self).get_access_action(cr, uid, ids, context=context) + self.ensure_one() + if not self.template_id: + return super(SaleOrder, self).get_access_action() return { 'type': 'ir.actions.act_url', - 'url': '/quote/%s/%s' % (quote.id, quote.access_token), + 'url': '/quote/%s/%s' % (self.id, self.access_token), 'target': 'self', - 'res_id': quote.id, + 'res_id': self.id, } - def _confirm_online_quote(self, cr, uid, order_id, tx, context=None): - """ Payment callback: validate the order and write tx details in chatter """ - order = self.browse(cr, uid, order_id, context=context) - + @api.multi + def _confirm_online_quote(self, transaction): + """ Payment callback: validate the order and write transaction details in chatter """ # create draft invoice if transaction is ok - if tx and tx.state == 'done': - if order.state in ['draft', 'sent']: - self.action_confirm(cr, SUPERUSER_ID, order.id, context=context) - message = _('Order paid by %s. Transaction: %s. Amount: %s.') % (tx.partner_id.name, tx.acquirer_reference, tx.amount) - self.message_post(cr, uid, order_id, body=message, context=context) + if transaction and transaction.state == 'done': + if self.state in ['draft', 'sent']: + self.sudo().action_confirm() + message = _('Order paid by %s. Transaction: %s. Amount: %s.') % (transaction.partner_id.name, transaction.acquirer_reference, transaction.amount) + self.message_post(body=message) return True return False - def create(self, cr, uid, values, context=None): - if not values.get('template_id'): - defaults = self.default_get(cr, uid, ['template_id'], context=context) - template_values = self.onchange_template_id(cr, uid, [], defaults.get('template_id'), partner=values.get('partner_id'), fiscal_position_id=values.get('fiscal_position'), context=context).get('value', {}) - values = dict(template_values, **values) - return super(sale_order, self).create(cr, uid, values, context=context) - - def action_confirm(self, cr, uid, ids, context=None): - res = super(sale_order, self).action_confirm(cr, uid, ids, context=context) - for order in self.browse(cr, uid, ids, context=context): + @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.pool['mail.template'].send_mail(cr, uid, order.template_id.mail_template_id.id, order.id, context=context) + self.template_id.mail_template_id.send_mail(order.id) return res - def _get_payment_type(self, cr, uid, ids, context=None): - order = self.browse(cr, uid, ids, context=context) - - if order.require_payment == 2: + @api.multi + def _get_payment_type(self): + self.ensure_one() + if self.require_payment == 2: return 'form_save' else: return 'form' -class sale_order_option(osv.osv): +class SaleOrderOption(models.Model): _name = "sale.order.option" _description = "Sale Options" _order = 'sequence, id' - _columns = { - 'order_id': fields.many2one('sale.order', 'Sale Order Reference', ondelete='cascade', select=True), - '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)]), - 'layout_category_id': fields.many2one('sale.layout_category', string='Section'), - 'website_description': fields.html('Line Description', sanitize=False, translate=html_translate), - 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')), - 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')), - 'uom_id': fields.many2one('product.uom', 'Unit of Measure ', required=True), - 'quantity': fields.float('Quantity', required=True, - digits_compute= dp.get_precision('Product UoS')), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of suggested product."), - } - _defaults = { - 'quantity': 1, - } - - # TODO master: to remove, replaced by onchange of the new api - def on_change_product_id(self, cr, uid, ids, product, uom_id=None, context=None): - vals, domain = {}, [] - if not product: - return vals - product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) - name = product_obj.name - if product_obj.description_sale: - name += '\n'+product_obj.description_sale - vals.update({ - 'price_unit': product_obj.list_price, - 'website_description': product_obj and (product_obj.quote_description or product_obj.website_description), - 'name': name, - 'uom_id': uom_id or product_obj.uom_id.id, - }) - uom_obj = self.pool.get('product.uom') - if vals['uom_id'] != product_obj.uom_id.id: - selected_uom = uom_obj.browse(cr, uid, vals['uom_id'], context=context) - new_price = uom_obj._compute_price(cr, uid, [product_obj.uom_id.id], vals['price_unit'], selected_uom) - vals['price_unit'] = new_price - if not uom_id: - domain = {'uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)]} - return {'value': vals, 'domain': domain} - - # TODO master: to remove, replaced by onchange of the new api - def product_uom_change(self, cr, uid, ids, product, uom_id, context=None): - context = context or {} - if not uom_id: - return {'value': {'price_unit': 0.0, 'uom_id': False}} - return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context) + order_id = fields.Many2one('sale.order', 'Sale Order Reference', ondelete='cascade', index=True) + 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)]) + layout_category_id = fields.Many2one('sale.layout_category', string='Section') + website_description = fields.Html('Line Description', sanitize=False, translate=html_translate) + price_unit = fields.Float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')) + discount = fields.Float('Discount (%)', digits_compute=dp.get_precision('Discount')) + uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True) + quantity = fields.Float('Quantity', required=True, digits_compute=dp.get_precision('Product UoS'), default=1) + sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of suggested product.") @api.onchange('product_id', 'uom_id') def _onchange_product_id(self): @@ -302,31 +235,28 @@ class sale_order_option(osv.osv): @api.multi def button_add_to_order(self): + self.ensure_one() order = self.order_id if order.state not in ['draft', 'sent']: return False - option = self - if option.product_id in [line.product_id for line in order.order_line]: - line = [line for line in order.order_line if line.product_id == option.product_id][0] - vals = { - 'product_uom_qty': line.product_uom_qty + 1, - } - line.write(vals) + order_line = self.order_line.filtered(lambda line: line.product_id == self.product_id) + if order_line: + order_line[0].product_uom_qty += 1 else: vals = { - 'price_unit': option.price_unit, - 'website_description': option.website_description, - 'name': option.name, + 'price_unit': self.price_unit, + 'website_description': self.website_description, + 'name': self.name, 'order_id': order.id, - 'product_id': option.product_id.id, - 'layout_category_id': option.layout_category_id.id, - 'product_uom_qty': option.quantity, - 'product_uom': option.uom_id.id, - 'discount': option.discount, + 'product_id': self.product_id.id, + 'layout_category_id': self.layout_category_id.id, + 'product_uom_qty': self.quantity, + 'product_uom': self.uom_id.id, + 'discount': self.discount, } - line = self.env['sale.order.line'].create(vals) + order_line = self.env['sale.order.line'].create(vals) + order_line._compute_tax_id() - self.env['sale.order.line']._compute_tax_id() - self.env['sale.order.option'].write({'line_id': line}) - return {'type':'ir.actions.client', 'tag':'reload'} + self.write({'line_id': order_line.id}) + return {'type': 'ir.actions.client', 'tag': 'reload'} diff --git a/addons/website_quote/models/sale_quote.py b/addons/website_quote/models/sale_quote.py index 1d5883dda9e..510c0d1ad70 100644 --- a/addons/website_quote/models/sale_quote.py +++ b/addons/website_quote/models/sale_quote.py @@ -1,124 +1,110 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api -from openerp.osv import osv, fields - -import openerp.addons.decimal_precision as dp -from openerp.tools.translate import html_translate +from odoo import api, fields, models +from odoo.tools.translate import html_translate +import odoo.addons.decimal_precision as dp -class sale_quote_template(osv.osv): +class SaleQuoteTemplate(models.Model): _name = "sale.quote.template" _description = "Sale Quotation Template" - _columns = { - 'name': fields.char('Quotation Template', required=True), - 'website_description': fields.html('Description', translate=html_translate, sanitize=False), - 'quote_line': fields.one2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True), - 'note': fields.text('Terms and conditions'), - 'options': fields.one2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True), - 'number_of_days': fields.integer('Quotation Duration', help='Number of days for the validity date computation of the quotation'), - 'require_payment': fields.selection([ - (0, 'Not mandatory on website quote validation'), - (1, 'Immediate after website order validation'), - (2, 'Immediate after website order validation and save a token'), - ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote"), - 'mail_template_id': fields.many2one('mail.template', 'Confirmation Mail', help="This e-mail template will be sent on confirmation. Leave empty to send nothing.") - } - def open_template(self, cr, uid, quote_id, context=None): + + name = fields.Char('Quotation Template', required=True) + website_description = fields.Html('Description', translate=html_translate, sanitize=False) + quote_line = fields.One2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True) + note = fields.Text('Terms and conditions') + options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True) + number_of_days = fields.Integer('Quotation Duration', + help='Number of days for the validity date computation of the quotation') + require_payment = fields.Selection([ + (0, 'Not mandatory on website quote validation'), + (1, 'Immediate after website order validation'), + (2, 'Immediate after website order validation and save a token'), + ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote") + mail_template_id = fields.Many2one('mail.template', 'Confirmation Mail', + help="This e-mail template will be sent on confirmation. Leave empty to send nothing.") + + @api.multi + def open_template(self): + self.ensure_one() return { 'type': 'ir.actions.act_url', 'target': 'self', - 'url': '/quote/template/%d' % quote_id[0] + 'url': '/quote/template/%d' % self.id } -class sale_quote_line(osv.osv): +class SaleQuoteLine(models.Model): _name = "sale.quote.line" _description = "Quotation Template Lines" - _columns = { - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale quote lines."), - 'quote_id': fields.many2one('sale.quote.template', 'Quotation Template Reference', required=True, ondelete='cascade', select=True), - 'name': fields.text('Description', required=True, translate=True), - 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True), - 'layout_category_id': fields.many2one('sale.layout_category', string='Section'), - 'website_description': fields.related('product_id', 'product_tmpl_id', 'quote_description', string='Line Description', type='html', translate=True), - 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')), - 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')), - 'product_uom_qty': fields.float('Quantity', required=True, digits_compute= dp.get_precision('Product UoS')), - 'product_uom_id': fields.many2one('product.uom', 'Unit of Measure ', required=True), - } _order = 'sequence, id' - _defaults = { - 'product_uom_qty': 1, - 'discount': 0.0, - 'sequence': 10, - } - def on_change_product_id(self, cr, uid, ids, product, uom_id=None, context=None): - vals, domain = {}, [] - product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) - name = product_obj.name - if product_obj.description_sale: - name += '\n' + product_obj.description_sale - vals.update({ - 'price_unit': product_obj.lst_price, - 'product_uom_id': product_obj.uom_id.id, - 'website_description': product_obj and (product_obj.quote_description or product_obj.website_description) or '', - 'name': name, - 'product_uom_id': uom_id or product_obj.uom_id.id, - }) - uom_obj = self.pool.get('product.uom') - if vals['product_uom_id'] != product_obj.uom_id.id: - selected_uom = uom_obj.browse(cr, uid, vals['product_uom_id'], context=context) - new_price = uom_obj._compute_price(cr, uid, [product_obj.uom_id.id], vals['price_unit'], selected_uom) - vals['price_unit'] = new_price - if not uom_id: - domain = {'product_uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)]} - return {'value': vals, 'domain': domain} - def product_uom_change(self, cr, uid, ids, product, uom_id, context=None): - context = context or {} - if not uom_id: - return {'value': {'price_unit': 0.0, 'uom_id': False}} - return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context) + 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, + ondelete='cascade', index=True) + name = fields.Text('Description', required=True, translate=True) + product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True) + layout_category_id = fields.Many2one('sale.layout_category', string='Section') + 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) + product_uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True) - def _inject_quote_description(self, cr, uid, values, context=None): + @api.onchange('product_id') + def _onchange_product_id(self): + self.ensure_one() + if self.product_id: + name = self.product_id.name + if self.product_id.description_sale: + name += '\n' + self.product_id.description_sale + 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} + + @api.onchange('product_uom_id') + def _onchange_product_uom(self): + if self.product_id and self.product_uom_id: + self.price_unit = self.product_id.uom_id._compute_price(self.product_id.lst_price, self.product_uom_id) + + @api.model + def create(self, values): + values = self._inject_quote_description(values) + return super(SaleQuoteLine, self).create(values) + + @api.multi + def write(self, values): + 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.pool['product.product'].browse(cr, uid, values['product_id'], context=context) + product = self.env['product.product'].browse(values['product_id']) values['website_description'] = product.quote_description or product.website_description or '' return values - def create(self, cr, uid, values, context=None): - values = self._inject_quote_description(cr, uid, values, context) - ret = super(sale_quote_line, self).create(cr, uid, values, context=context) - # hack because create don t make the job for a related field - if values.get('website_description'): - self.write(cr, uid, ret, {'website_description': values['website_description']}, context=context) - return ret - def write(self, cr, uid, ids, values, context=None): - values = self._inject_quote_description(cr, uid, values, context) - return super(sale_quote_line, self).write(cr, uid, ids, values, context=context) - - -class sale_quote_option(osv.osv): +class SaleQuoteOption(models.Model): _name = "sale.quote.option" _description = "Quotation Option" - _columns = { - 'template_id': fields.many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade', select=True, required=True), - 'name': fields.text('Description', required=True, translate=True), - 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True), - 'layout_category_id': fields.many2one('sale.layout_category', string='Section'), - 'website_description': fields.html('Option Description', translate=html_translate, sanitize=False), - 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')), - 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')), - 'uom_id': fields.many2one('product.uom', 'Unit of Measure ', required=True), - 'quantity': fields.float('Quantity', required=True, digits_compute= dp.get_precision('Product UoS')), - } - _defaults = { - 'quantity': 1, - } + + template_id = fields.Many2one('sale.quote.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) + layout_category_id = fields.Many2one('sale.layout_category', string='Section') + website_description = fields.Html('Option Description', translate=html_translate, sanitize=False) + price_unit = fields.Float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')) + discount = fields.Float('Discount (%)', digits_compute=dp.get_precision('Discount')) + uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True) + quantity = fields.Float('Quantity', required=True, digits_compute=dp.get_precision('Product UoS'), default=1) @api.onchange('product_id') def _onchange_product_id(self): @@ -140,32 +126,4 @@ class sale_quote_option(osv.osv): self.price_unit = 0.0 return if self.uom_id.id != self.product_id.uom_id.id: - new_price = self.product_id.uom_id._compute_price(self.price_unit, self.uom_id) - self.price_unit = new_price - - def on_change_product_id(self, cr, uid, ids, product, uom_id=None, context=None): - vals, domain = {}, [] - product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) - name = product_obj.name - if product_obj.description_sale: - name += '\n' + product_obj.description_sale - vals.update({ - 'price_unit': product_obj.list_price, - 'website_description': product_obj.product_tmpl_id.quote_description, - 'name': name, - 'uom_id': uom_id or product_obj.uom_id.id, - }) - uom_obj = self.pool.get('product.uom') - if vals['uom_id'] != product_obj.uom_id.id: - selected_uom = uom_obj.browse(cr, uid, vals['uom_id'], context=context) - new_price = uom_obj._compute_price(cr, uid, [product_obj.uom_id.id], - vals['price_unit'], selected_uom) - vals['price_unit'] = new_price - if not uom_id: - domain = {'uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)]} - return {'value': vals, 'domain': domain} - - def product_uom_change(self, cr, uid, ids, product, uom_id, context=None): - if not uom_id: - return {'value': {'price_unit': 0.0, 'uom_id': False}} - return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context) + self.price_unit = self.product_id.uom_id._compute_price(self.price_unit, self.uom_id) diff --git a/addons/website_quote/report/sale_order_reports.xml b/addons/website_quote/report/sale_order_reports.xml index b9b363b06c6..9b4f6e10082 100644 --- a/addons/website_quote/report/sale_order_reports.xml +++ b/addons/website_quote/report/sale_order_reports.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/report/sale_order_templates.xml b/addons/website_quote/report/sale_order_templates.xml index b0bfcbdb5a1..8a6f824ae5c 100644 --- a/addons/website_quote/report/sale_order_templates.xml +++ b/addons/website_quote/report/sale_order_templates.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/report/website_quote_templates.xml b/addons/website_quote/report/website_quote_templates.xml index 55c8ddcc771..db73b677770 100644 --- a/addons/website_quote/report/website_quote_templates.xml +++ b/addons/website_quote/report/website_quote_templates.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/views/sale_order_views.xml b/addons/website_quote/views/sale_order_views.xml index 70561001337..4c679196d12 100644 --- a/addons/website_quote/views/sale_order_views.xml +++ b/addons/website_quote/views/sale_order_views.xml @@ -1,6 +1,6 @@ - + sale.order.form.payment sale.order @@ -33,7 +33,6 @@