[MERGE] website_quote: migration to new API

This commit is contained in:
Thibault Delavallée
2016-08-05 15:28:21 +02:00
17 changed files with 611 additions and 753 deletions
+3
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
import models
+8 -5
View File
@@ -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
+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')
}
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>
+6 -1
View File
@@ -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
-506
View File
@@ -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)
+253
View File
@@ -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'}
+129
View File
@@ -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)
@@ -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>
@@ -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>
@@ -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>
@@ -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>