[MIG] website_quote: migrate to new api

This commit is contained in:
Denis Vermylen (dve)
2016-08-05 15:25:26 +02:00
committed by Thibault Delavallée
parent 1fe106ef7a
commit 232eea36ab
12 changed files with 378 additions and 536 deletions
+116 -134
View File
@@ -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')
}
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/<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,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)
+153 -223
View File
@@ -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'}
+82 -124
View File
@@ -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)
@@ -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>
@@ -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>
@@ -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>
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="sale_order_form_quote">
<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"/>
@@ -33,7 +33,6 @@
<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>
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<openerp>
<data>
<?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>
@@ -29,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>
@@ -54,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"/>
@@ -79,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"/>
@@ -105,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>
@@ -132,6 +118,5 @@
</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>
@@ -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">
@@ -709,6 +708,4 @@
</t>
</xpath>
</template>
</data>
</openerp>
</odoo>