[MERGE] website_quote: migration to new API
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import controllers
|
||||
import models
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Online Proposals',
|
||||
'category': 'Website',
|
||||
@@ -11,12 +13,13 @@ Odoo Sale Quote Roller
|
||||
""",
|
||||
'depends': ['website', 'sale', 'mail', 'payment', 'website_portal_sale', 'website_mail'],
|
||||
'data': [
|
||||
'quotation_report.xml',
|
||||
'views/website_quotation.xml',
|
||||
'views/website_quotation_backend.xml',
|
||||
'views/report_saleorder.xml',
|
||||
'views/report_quotation.xml',
|
||||
'data/website_quotation_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',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import main
|
||||
|
||||
@@ -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/<int:order_id>/<token>"
|
||||
], 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')
|
||||
}
|
||||
values['buttons'] = {}
|
||||
for acquirer in values['acquirers']:
|
||||
acquirer.button = payment_obj.render(
|
||||
request.cr, SUPERUSER_ID, acquirer.id,
|
||||
values['buttons'][acquirer.id] = 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/<int:order_id>/<token>/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/<model('sale.quote.template'):quote>"], 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/<int:option_id>/<int:order_id>/<token>"], 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/<int:order_id>/transaction/<int:acquirer_id>'], 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,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<openerp>
|
||||
<?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">
|
||||
@@ -148,4 +149,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<record id="product_template_quote_1" model="product.template">
|
||||
<field name="name">Functional Training</field>
|
||||
<field name="categ_id" ref="product.product_category_3"/>
|
||||
@@ -523,6 +522,4 @@
|
||||
</section>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
import order
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import sale_order
|
||||
import sale_quote
|
||||
import product_template
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class sale_quote_template(osv.osv):
|
||||
_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):
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'self',
|
||||
'url': '/quote/template/%d' % quote_id[0]
|
||||
}
|
||||
|
||||
|
||||
class sale_quote_line(osv.osv):
|
||||
_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)
|
||||
|
||||
def _inject_quote_description(self, cr, uid, values, context=None):
|
||||
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)
|
||||
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_order_line(osv.osv):
|
||||
_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):
|
||||
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)
|
||||
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):
|
||||
_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
|
||||
|
||||
_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"),
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
_defaults = {
|
||||
'access_token': lambda self, cr, uid, ctx={}: str(uuid.uuid4()),
|
||||
'template_id': _get_template_id,
|
||||
}
|
||||
|
||||
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:
|
||||
res = self.pool.get('sale.order.line').product_id_change(cr, uid, False,
|
||||
False, line.product_id.id, line.product_uom_qty, line.product_uom_id.id, line.product_uom_qty,
|
||||
line.product_uom_id.id, line.name, partner, False, True, time.strftime('%Y-%m-%d'),
|
||||
False, fiscal_position_id, True, context)
|
||||
data = res.get('value', {})
|
||||
if pricelist_id:
|
||||
uom_context = context.copy()
|
||||
uom_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)
|
||||
else:
|
||||
price = line.price_unit
|
||||
|
||||
if 'tax_id' in data:
|
||||
data['tax_id'] = [(6, 0, data['tax_id'])]
|
||||
else:
|
||||
fpos = (fiscal_position_id and self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position_id)) or False
|
||||
taxes = fpos.map_tax(line.product_id.product_tmpl_id.taxes_id).ids if fpos else line.product_id.product_tmpl_id.taxes_id.ids
|
||||
data['tax_id'] = [(6, 0, taxes)]
|
||||
data.update({
|
||||
'name': line.name,
|
||||
'price_unit': price,
|
||||
'discount': line.discount,
|
||||
'product_uom_qty': line.product_uom_qty,
|
||||
'product_id': line.product_id.id,
|
||||
'layout_category_id': line.layout_category_id,
|
||||
'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),
|
||||
})
|
||||
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)
|
||||
else:
|
||||
price = option.price_unit
|
||||
options.append((0, 0, {
|
||||
'product_id': option.product_id.id,
|
||||
'layout_category_id': option.layout_category_id,
|
||||
'name': option.name,
|
||||
'quantity': option.quantity,
|
||||
'uom_id': option.uom_id.id,
|
||||
'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}
|
||||
|
||||
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')
|
||||
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):
|
||||
""" 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)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/quote/%s/%s' % (quote.id, quote.access_token),
|
||||
'target': 'self',
|
||||
'res_id': quote.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)
|
||||
|
||||
# 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)
|
||||
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):
|
||||
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)
|
||||
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:
|
||||
return 'form_save'
|
||||
else:
|
||||
return 'form'
|
||||
|
||||
class sale_quote_option(osv.osv):
|
||||
_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,
|
||||
}
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if not self.product_id:
|
||||
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)]}
|
||||
return {'domain': domain}
|
||||
|
||||
@api.onchange('uom_id')
|
||||
def _onchange_product_uom(self):
|
||||
if not self.product_id:
|
||||
return
|
||||
if not self.uom_id:
|
||||
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)
|
||||
|
||||
|
||||
class sale_order_option(osv.osv):
|
||||
_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)
|
||||
|
||||
@api.onchange('product_id', 'uom_id')
|
||||
def _onchange_product_id(self):
|
||||
if not self.product_id:
|
||||
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
|
||||
self.uom_id = self.uom_id or product.uom_id
|
||||
pricelist = self.order_id.pricelist_id
|
||||
if pricelist and product:
|
||||
partner_id = self.order_id.partner_id.id
|
||||
self.price_unit = pricelist.with_context(uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id)
|
||||
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
return {'domain': domain}
|
||||
|
||||
@api.multi
|
||||
def button_add_to_order(self):
|
||||
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)
|
||||
else:
|
||||
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,
|
||||
}
|
||||
line = self.env['sale.order.line'].create(vals)
|
||||
|
||||
self.env['sale.order.line']._compute_tax_id()
|
||||
self.env['sale.order.option'].write({'line_id': line})
|
||||
return {'type':'ir.actions.client', 'tag':'reload'}
|
||||
|
||||
|
||||
class product_template(osv.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
_columns = {
|
||||
'website_description': fields.html('Description for the website', sanitize=False, translate=html_translate), # hack, if website_sale is not installed
|
||||
'quote_description': fields.html('Description for the quote', sanitize=False, translate=html_translate),
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,253 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.translate import html_translate
|
||||
import odoo.addons.decimal_precision as dp
|
||||
|
||||
|
||||
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.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.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_id(self):
|
||||
return self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
|
||||
|
||||
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")
|
||||
|
||||
@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('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)
|
||||
|
||||
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
|
||||
|
||||
data = {
|
||||
'name': line.name,
|
||||
'price_unit': price,
|
||||
'discount': line.discount,
|
||||
'product_uom_qty': line.product_uom_qty,
|
||||
'product_id': line.product_id.id,
|
||||
'layout_category_id': line.layout_category_id,
|
||||
'product_uom': line.product_uom_id.id,
|
||||
'website_description': line.website_description,
|
||||
'state': 'draft',
|
||||
'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id),
|
||||
}
|
||||
order_lines.append((0, 0, data))
|
||||
|
||||
self.order_line = order_lines
|
||||
self.order_line._compute_tax_id()
|
||||
|
||||
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
|
||||
data = {
|
||||
'product_id': option.product_id.id,
|
||||
'layout_category_id': option.layout_category_id,
|
||||
'name': option.name,
|
||||
'quantity': option.quantity,
|
||||
'uom_id': option.uom_id.id,
|
||||
'price_unit': price,
|
||||
'discount': option.discount,
|
||||
'website_description': option.website_description,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@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. """
|
||||
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' % (self.id, self.access_token),
|
||||
'target': 'self',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
@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 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
|
||||
|
||||
@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)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _get_payment_type(self):
|
||||
self.ensure_one()
|
||||
if self.require_payment == 2:
|
||||
return 'form_save'
|
||||
else:
|
||||
return 'form'
|
||||
|
||||
|
||||
class SaleOrderOption(models.Model):
|
||||
_name = "sale.order.option"
|
||||
_description = "Sale Options"
|
||||
_order = 'sequence, id'
|
||||
|
||||
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):
|
||||
if not self.product_id:
|
||||
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
|
||||
self.uom_id = self.uom_id or product.uom_id
|
||||
pricelist = self.order_id.pricelist_id
|
||||
if pricelist and product:
|
||||
partner_id = self.order_id.partner_id.id
|
||||
self.price_unit = pricelist.with_context(uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id)
|
||||
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
return {'domain': domain}
|
||||
|
||||
@api.multi
|
||||
def button_add_to_order(self):
|
||||
self.ensure_one()
|
||||
order = self.order_id
|
||||
if order.state not in ['draft', 'sent']:
|
||||
return False
|
||||
|
||||
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': self.price_unit,
|
||||
'website_description': self.website_description,
|
||||
'name': self.name,
|
||||
'order_id': order.id,
|
||||
'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,
|
||||
}
|
||||
order_line = self.env['sale.order.line'].create(vals)
|
||||
order_line._compute_tax_id()
|
||||
|
||||
self.write({'line_id': order_line.id})
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- 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
|
||||
import odoo.addons.decimal_precision as dp
|
||||
|
||||
|
||||
class SaleQuoteTemplate(models.Model):
|
||||
_name = "sale.quote.template"
|
||||
_description = "Sale Quotation Template"
|
||||
|
||||
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' % self.id
|
||||
}
|
||||
|
||||
|
||||
class SaleQuoteLine(models.Model):
|
||||
_name = "sale.quote.line"
|
||||
_description = "Quotation Template Lines"
|
||||
_order = '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,
|
||||
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)
|
||||
|
||||
@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.env['product.product'].browse(values['product_id'])
|
||||
values['website_description'] = product.quote_description or product.website_description or ''
|
||||
return values
|
||||
|
||||
|
||||
class SaleQuoteOption(models.Model):
|
||||
_name = "sale.quote.option"
|
||||
_description = "Quotation Option"
|
||||
|
||||
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):
|
||||
if not self.product_id:
|
||||
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)]}
|
||||
return {'domain': domain}
|
||||
|
||||
@api.onchange('uom_id')
|
||||
def _onchange_product_uom(self):
|
||||
if not self.product_id:
|
||||
return
|
||||
if not self.uom_id:
|
||||
self.price_unit = 0.0
|
||||
return
|
||||
if self.uom_id.id != self.product_id.uom_id.id:
|
||||
self.price_unit = self.product_id.uom_id._compute_price(self.price_unit, self.uom_id)
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<report
|
||||
id="report_web_quote"
|
||||
string="Web Quotation"
|
||||
@@ -10,5 +9,4 @@
|
||||
name="website_quote.report_quote"
|
||||
menu="False"
|
||||
/>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<template id="report_saleorder_validity_date" inherit_id="sale.report_saleorder_document">
|
||||
<xpath expr="//div[@class='page']/div[@id='informations']" position="inside">
|
||||
<div t-if="doc.validity_date" class="col-xs-3">
|
||||
@@ -52,5 +51,4 @@
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<template id="report_quote">
|
||||
<t t-call="report.html_container">
|
||||
<t t-set="data_report_margin_top" t-value="45"/>
|
||||
@@ -15,5 +14,4 @@
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="sale_order_form_quote" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.payment</field>
|
||||
<field name="model">sale.order</field>
|
||||
<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)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//page/field[@name='order_line']/.." position="after">
|
||||
<page string="Suggested Products">
|
||||
<field name="quote_viewed" invisible="1"/>
|
||||
<field name="options">
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id"/>
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<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>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='client_order_ref']" position="after">
|
||||
<field name="require_payment" widget="radio"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale.action_quotations" model="ir.actions.act_window">
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Create a Quotation, the first step of a new sale.
|
||||
</p><p>
|
||||
Your next actions should flow efficiently: confirm the Quotation
|
||||
to a Sale Order, then create the Invoice and collect the Payment.
|
||||
</p><p>
|
||||
Note that once a Quotation becomes a Sale Order, it will be moved
|
||||
from the Quotations list to the Sales Order list.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
+8
-81
@@ -1,50 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="sale_order_form_quote">
|
||||
<field name="name">sale.order.form.payment</field>
|
||||
<field name="model">sale.order</field>
|
||||
<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)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//page/field[@name='order_line']/.." position="after">
|
||||
<page string="Suggested Products">
|
||||
<field name="quote_viewed" invisible="1"/>
|
||||
<field name="options">
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id"/>
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<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}"
|
||||
on_change="onchange_template_id(template_id, partner_id, fiscal_position_id, pricelist_id)"
|
||||
options="{'no_create': True, 'no_open': True}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='client_order_ref']" position="after">
|
||||
<field name="require_payment" widget="radio"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<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>
|
||||
@@ -73,7 +28,7 @@
|
||||
<form string="Sales Quotation Template Lines">
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" on_change="on_change_product_id(product_id, product_uom_id)"/>
|
||||
<field name="product_id"/>
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
<label for="product_uom_qty"/>
|
||||
<div>
|
||||
@@ -98,11 +53,11 @@
|
||||
</form>
|
||||
<tree string="Sales Quotation Template Lines" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id" on_change="on_change_product_id(product_id, product_uom_id)"/>
|
||||
<field name="product_id"/>
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
<field name="name"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom_id" groups="product.group_uom" on_change="product_uom_change(product_id, product_uom_id"/>
|
||||
<field name="product_uom_id" groups="product.group_uom"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<field name="website_description" invisible="1"/>
|
||||
@@ -123,19 +78,6 @@
|
||||
</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="product.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="Other Settings">
|
||||
<group>
|
||||
<field name="require_payment" widget="radio"/>
|
||||
@@ -149,7 +91,7 @@
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="view_sale_quote_template_tree">
|
||||
<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>
|
||||
@@ -176,20 +118,5 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale.action_quotations" model="ir.actions.act_window">
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Create a Quotation, the first step of a new sale.
|
||||
</p><p>
|
||||
Your next actions should flow efficiently: confirm the Quotation
|
||||
to a Sale Order, then create the Invoice and collect the Payment.
|
||||
</p><p>
|
||||
Note that once a Quotation becomes a Sale Order, it will be moved
|
||||
from the Quotations list to the Sales Order list.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_sale_quotation_template" id="menu_sale_quote_template" parent="sales_team.menu_sales_config" sequence="1" name="Quotation Templates" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
|
||||
</data>
|
||||
</openerp>
|
||||
<menuitem id="menu_sale_quote_template" action="action_sale_quotation_template" parent="sales_team.menu_sales_config" sequence="1" name="Quotation Templates" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
|
||||
</odoo>
|
||||
+4
-7
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website Quote frontend assets">
|
||||
<xpath expr="." position="inside">
|
||||
@@ -334,7 +333,7 @@
|
||||
<h4>Payment Method:</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li t-foreach="acquirers or []" t-as="acquirer">
|
||||
<label t-if="acquirer.button">
|
||||
<label t-if="buttons.get(acquirer.id)">
|
||||
<input t-att-value="acquirer.id" type="radio" name="acquirer" t-att-checked="acquirers[0] == acquirer"/>
|
||||
<img class="media-object" style="width: 60px; display: inline-block;"
|
||||
t-att-title="acquirer.name"
|
||||
@@ -347,7 +346,7 @@
|
||||
<div class="col-md-6">
|
||||
<t t-foreach="acquirers or []" t-as="acquirer">
|
||||
<div t-att-data-id="acquirer.id" class="oe_quote_acquirer_button hidden pull-right">
|
||||
<div t-raw="acquirer.button"/>
|
||||
<div t-raw="buttons.get(acquirer.id)"/>
|
||||
<div t-field="acquirer.pre_msg"/>
|
||||
<button type="button" class="btn btn-link" data-dismiss="modal" style="padding: 0">Cancel</button>
|
||||
</div>
|
||||
@@ -709,6 +708,4 @@
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user