diff --git a/addons/website_quote/__init__.py b/addons/website_quote/__init__.py index 9f86759e32b..ac0011716f6 100644 --- a/addons/website_quote/__init__.py +++ b/addons/website_quote/__init__.py @@ -1,2 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + import controllers import models diff --git a/addons/website_quote/__openerp__.py b/addons/website_quote/__openerp__.py index f1fc19fac67..7c18b07f0c7 100644 --- a/addons/website_quote/__openerp__.py +++ b/addons/website_quote/__openerp__.py @@ -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': [ diff --git a/addons/website_quote/controllers/__init__.py b/addons/website_quote/controllers/__init__.py index 8ee9bae18d9..f48716d3a69 100644 --- a/addons/website_quote/controllers/__init__.py +++ b/addons/website_quote/controllers/__init__.py @@ -1 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + import main diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index c40d6d47484..805e34d7257 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import SUPERUSER_ID -from openerp import http -from openerp.http import request import werkzeug -import datetime -import time -from openerp.tools.translate import _ -from openerp.addons.website_mail.controllers.main import _message_post_helper +from odoo import fields, http, _ +from odoo.http import request +from odoo.addons.website_mail.controllers.main import _message_post_helper class sale_quote(http.Controller): @@ -18,96 +14,93 @@ class sale_quote(http.Controller): "/quote//" ], type='http', auth="public", website=True) def view(self, order_id, pdf=None, token=None, message=False, **post): - # use SUPERUSER_ID allow to access/view order for public user + # use sudo to allow accessing/viewing orders for public user # only if he knows the private token - now = time.strftime('%Y-%m-%d') + now = fields.Date.today() if token: - order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)]) + Order = request.env['sale.order'].sudo().search([('id', '=', order_id), ('access_token', '=', token)]) # Log only once a day - if order and request.session.get('view_quote', False) != now: + if Order and request.session.get('view_quote') != now: request.session['view_quote'] = now body = _('Quotation viewed by customer') - _message_post_helper(res_model='sale.order', res_id=order.id, message=body, token=token, token_field="access_token", message_type='notification') + _message_post_helper(res_model='sale.order', res_id=Order.id, message=body, token=token, token_field="access_token", message_type='notification') else: - order = request.env['sale.order'].search([('id', '=', order_id)]) + Order = request.env['sale.order'].search([('id', '=', order_id)]) - if not order: + if not Order: return request.render('website.404') - dummy, action = request.env['ir.model.data'].get_object_reference('sale', 'action_quotations') days = 0 - if order.validity_date: - days = (datetime.datetime.strptime(order.validity_date, '%Y-%m-%d') - datetime.datetime.now()).days + 1 + if Order.validity_date: + days = (fields.Date.from_string(Order.validity_date) - fields.Date.from_string(fields.Date.today())).days + 1 if pdf: - report_obj = request.registry['report'] - pdf = report_obj.get_pdf(request.cr, SUPERUSER_ID, [order_id], 'website_quote.report_quote', data=None, context=dict(request.context, set_viewport_size=True)) + pdf = request.env['report'].sudo().with_context(set_viewport_size=True).get_pdf(Order.id, 'website_quote.report_quote') pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) - user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) - tx_id = request.session.get('quote_%s_transaction_id' % order.id) - if not tx_id: - tx_id = request.registry['payment.transaction'].search(request.cr, SUPERUSER_ID, [('reference', '=', order.name)], context=request.context) - tx = request.registry['payment.transaction'].browse(request.cr, SUPERUSER_ID, tx_id, context=request.context) if tx_id else False + transaction_id = request.session.get('quote_%s_transaction_id' % Order.id) + if not transaction_id: + Transaction = request.env['payment.transaction'].sudo().search([('reference', '=', Order.name)]) + else: + Transaction = request.env['payment.transaction'].sudo().browse(transaction_id) values = { - 'quotation': order, + 'quotation': Order, 'message': message and int(message) or False, - 'option': bool(filter(lambda x: not x.line_id, order.options)), - 'order_valid': (not order.validity_date) or (now <= order.validity_date), + 'option': bool(filter(lambda x: not x.line_id, Order.options)), + 'order_valid': (not Order.validity_date) or (now <= Order.validity_date), 'days_valid': days, - 'action': action, - 'breadcrumb': user.partner_id == order.partner_id, - 'tx_id': tx_id, - 'tx_state': tx.state if tx else False, - 'tx_post_msg': tx.acquirer_id.post_msg if tx else False, - 'need_payment': order.invoice_status == 'to invoice' and (not tx or tx.state in ['draft', 'cancel', 'error']), + 'action': request.env.ref('sale.action_quotations').id, + 'breadcrumb': request.env.user.partner_id == Order.partner_id, + 'tx_id': Transaction.id if Transaction else False, + 'tx_state': Transaction.state if Transaction else False, + 'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False, + 'need_payment': Order.invoice_status == 'to invoice' and Transaction.state in ['draft', 'cancel', 'error'], 'token': token, } - if order.require_payment or values['need_payment']: - payment_obj = request.registry.get('payment.acquirer') - acquirer_ids = payment_obj.search(request.cr, SUPERUSER_ID, [('website_published', '=', True), ('company_id', '=', order.company_id.id)], context=request.context) - values['acquirers'] = list(payment_obj.browse(request.cr, token and SUPERUSER_ID or request.uid, acquirer_ids, context=request.context)) - render_ctx = dict(request.context, submit_class='btn btn-primary', submit_txt=_('Pay & Confirm')) + if Order.require_payment or values['need_payment']: + if token: + values['acquirers'] = list(request.env['payment.acquirer'].sudo().search([('website_published', '=', True), ('company_id', '=', Order.company_id.id)])) + else: + values['acquirers'] = list(request.env['payment.acquirer'].sudo(user=request.uid).search([('website_published', '=', True), ('company_id', '=', Order.company_id.id)])) + extra_context = { + 'submit_class': 'btn btn-primary', + 'submit_txt': _('Pay & Confirm') + } + 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///decline'], type='http', auth="public", methods=['POST'], website=True) def decline(self, order_id, token, **post): - order_obj = request.registry.get('sale.order') - order = order_obj.browse(request.cr, SUPERUSER_ID, order_id) - if token != order.access_token: + Order = request.env['sale.order'].sudo().browse(order_id) + if token != Order.access_token: return request.render('website.404') - if order.state != 'sent': + if Order.state != 'sent': return werkzeug.utils.redirect("/quote/%s/%s?message=4" % (order_id, token)) - request.registry.get('sale.order').action_cancel(request.cr, SUPERUSER_ID, [order_id]) + Order.action_cancel() message = post.get('decline_message') if message: _message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': token, 'token_field': 'access_token'} if token else {}) @@ -115,55 +108,49 @@ class sale_quote(http.Controller): @http.route(['/quote/update_line'], type='json', auth="public", website=True) def update(self, line_id, remove=False, unlink=False, order_id=None, token=None, **post): - order = request.registry.get('sale.order').browse(request.cr, SUPERUSER_ID, int(order_id)) - if token != order.access_token: + Order = request.env['sale.order'].sudo().browse(int(order_id)) + if token != Order.access_token: return request.render('website.404') - if order.state not in ('draft','sent'): + if Order.state not in ('draft', 'sent'): return False - line_id=int(line_id) + OrderLine = request.env['sale.order.line'].sudo().browse(int(line_id)) if unlink: - request.registry.get('sale.order.line').unlink(request.cr, SUPERUSER_ID, [line_id], context=request.context) + OrderLine.unlink() return False - number=(remove and -1 or 1) - - order_line_obj = request.registry.get('sale.order.line') - order_line_val = order_line_obj.read(request.cr, SUPERUSER_ID, [line_id], [], context=request.context)[0] - quantity = order_line_val['product_uom_qty'] + number - order_line_obj.write(request.cr, SUPERUSER_ID, [line_id], {'product_uom_qty': (quantity)}, context=request.context) - return [str(quantity), str(order.amount_total)] + number = -1 if remove else 1 + quantity = OrderLine.product_uom_qty + number + OrderLine.write({'product_uom_qty': quantity}) + return [str(quantity), str(Order.amount_total)] @http.route(["/quote/template/"], type='http', auth="user", website=True) def template_view(self, quote, **post): - values = { 'template': quote } + values = {'template': quote} return request.render('website_quote.so_template', values) @http.route(["/quote/add_line///"], type='http', auth="public", website=True) def add(self, option_id, order_id, token, **post): - vals = {} - order = request.registry.get('sale.order').browse(request.cr, SUPERUSER_ID, order_id) - if token != order.access_token: + Order = request.env['sale.order'].sudo().browse(order_id) + if token != Order.access_token: return request.render('website.404') - if order.state not in ['draft', 'sent']: + if Order.state not in ['draft', 'sent']: return request.render('website.http_error', {'status_code': 'Forbidden', 'status_message': _('You cannot add options to a confirmed order.')}) - option_obj = request.registry.get('sale.order.option') - option = option_obj.browse(request.cr, SUPERUSER_ID, option_id) - + Option = request.env['sale.order.option'].sudo().browse(option_id) vals = { - 'price_unit': option.price_unit, - 'website_description': option.website_description, - 'name': option.name, - 'order_id': order.id, - 'product_id': option.product_id.id, - 'layout_category_id': option.layout_category_id.id, - 'product_uom_qty': option.quantity, - 'product_uom': option.uom_id.id, - 'discount': option.discount, + 'price_unit': Option.price_unit, + 'website_description': Option.website_description, + 'name': Option.name, + 'order_id': Order.id, + 'product_id': Option.product_id.id, + 'layout_category_id': Option.layout_category_id.id, + 'product_uom_qty': Option.quantity, + 'product_uom': Option.uom_id.id, + 'discount': Option.discount, } - line = request.registry.get('sale.order.line').create(request.cr, SUPERUSER_ID, vals, context=request.context) - request.registry.get('sale.order.line')._compute_tax_id(request.cr, SUPERUSER_ID, [line], context=request.context) - option_obj.write(request.cr, SUPERUSER_ID, [option.id], {'line_id': line}, context=request.context) - return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (order.id, token)) + OrderLine = request.env['sale.order.line'].sudo().create(vals) + OrderLine._compute_tax_id() + Option.write({'line_id': OrderLine}) + return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (Order.id, token)) # note dbo: website_sale code @http.route(['/quote//transaction/'], type='json', auth="public", website=True) @@ -180,55 +167,50 @@ class sale_quote(http.Controller): :param int acquirer_id: id of a payment.acquirer record. If not set the user is redirected to the checkout page """ - cr, uid, context = request.cr, request.uid, request.context - payment_obj = request.registry.get('payment.acquirer') - transaction_obj = request.registry.get('payment.transaction') - order = request.registry.get('sale.order').browse(cr, SUPERUSER_ID, order_id, context=context) + PaymentTransaction = request.env['payment.transaction'].sudo() - if not order or not order.order_line or acquirer_id is None: - return request.redirect("/quote/" + str(order_id)) + Order = request.env['sale.order'].sudo().browse(order_id) + if not Order or not Order.order_line or acquirer_id is None: + return request.redirect("/quote/%s" % order_id) # find an already existing transaction - tx_id = transaction_obj.search(cr, SUPERUSER_ID, [('reference', '=', order.name)], context=context) - tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) - if tx: - if tx.sale_order_id.id != order.id or tx.state in ['error', 'cancel'] or tx.acquirer_id.id != acquirer_id: - tx = False - tx_id = False - elif tx.state == 'draft': - tx.write({ - 'amount': order.amount_total, + Transaction = PaymentTransaction.search([('reference', '=', Order.name)]) + if Transaction: + if Transaction.sale_order_id != Order or Transaction.state in ['error', 'cancel'] or Transaction.acquirer_id.id != acquirer_id: + Transaction = False + elif Transaction.state == 'draft': + Transaction.write({ + 'amount': Order.amount_total, }) - if not tx: - tx_id = transaction_obj.create(cr, SUPERUSER_ID, { + if not Transaction: + Transaction = PaymentTransaction.create({ 'acquirer_id': acquirer_id, - 'type': order._get_payment_type(), - 'amount': order.amount_total, - 'currency_id': order.pricelist_id.currency_id.id, - 'partner_id': order.partner_id.id, - 'reference': transaction_obj.get_next_reference(cr, uid, order.name, context=context), - 'sale_order_id': order.id, - 'callback_eval': "self.env['sale.order']._confirm_online_quote(self.sale_order_id.id, self)" - }, context=context) - request.session['quote_%s_transaction_id' % order.id] = tx_id - tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) - # update quotation - request.registry['sale.order'].write( - cr, SUPERUSER_ID, [order.id], { - 'payment_acquirer_id': acquirer_id, - 'payment_tx_id': tx_id - }, context=context) + 'type': Order._get_payment_type(), + 'amount': Order.amount_total, + 'currency_id': Order.pricelist_id.currency_id.id, + 'partner_id': Order.partner_id.id, + 'reference': PaymentTransaction.get_next_reference(Order.name), + 'sale_order_id': Order.id, + 'callback_eval': "self.sale_order_id._confirm_online_quote(self)" + }) + request.session['quote_%s_transaction_id' % Order.id] = Transaction.id - return payment_obj.render( - request.cr, SUPERUSER_ID, tx.acquirer_id.id, - tx.reference, - order.amount_total, - order.pricelist_id.currency_id.id, + # update quotation + Order.write({ + 'payment_acquirer_id': acquirer_id, + 'payment_tx_id': Transaction.id + }) + + return Transaction.acquirer_id.with_context( + submit_class='btn btn-primary', + submit_txt=_('Pay & Confirm')).render( + Transaction.reference, + Order.amount_total, + Order.pricelist_id.currency_id.id, values={ 'return_url': '/quote/%s/%s' % (order_id, token) if token else '/quote/%s' % order_id, - 'type': order._get_payment_type(), + 'type': Order._get_payment_type(), 'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'), - 'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id, - 'billing_partner_id': order.partner_invoice_id.id, - }, - context=dict(context, submit_class='btn btn-primary', submit_txt=_('Pay & Confirm'))) + 'partner_id': Order.partner_shipping_id.id or Order.partner_invoice_id.id, + 'billing_partner_id': Order.partner_invoice_id.id, + }) diff --git a/addons/website_quote/data/website_quotation_data.xml b/addons/website_quote/data/website_quotation_data.xml index 1e28def4549..8e0c4a73e6f 100644 --- a/addons/website_quote/data/website_quotation_data.xml +++ b/addons/website_quote/data/website_quotation_data.xml @@ -1,4 +1,5 @@ - + + @@ -148,4 +149,4 @@ - + diff --git a/addons/website_quote/data/website_quotation_demo.xml b/addons/website_quote/data/website_quotation_demo.xml index 41ecd810a9f..90973a1ae50 100644 --- a/addons/website_quote/data/website_quotation_demo.xml +++ b/addons/website_quote/data/website_quotation_demo.xml @@ -1,6 +1,5 @@ - - + Functional Training @@ -523,6 +522,4 @@ - - - + diff --git a/addons/website_quote/models/__init__.py b/addons/website_quote/models/__init__.py index 49178edb61d..011bafd821f 100644 --- a/addons/website_quote/models/__init__.py +++ b/addons/website_quote/models/__init__.py @@ -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 diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py deleted file mode 100644 index 25f47f54d47..00000000000 --- a/addons/website_quote/models/order.py +++ /dev/null @@ -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), - } diff --git a/addons/website_quote/models/product_template.py b/addons/website_quote/models/product_template.py new file mode 100644 index 00000000000..ef2773225f1 --- /dev/null +++ b/addons/website_quote/models/product_template.py @@ -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) diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py new file mode 100644 index 00000000000..c0eb2fb1380 --- /dev/null +++ b/addons/website_quote/models/sale_order.py @@ -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'} diff --git a/addons/website_quote/models/sale_quote.py b/addons/website_quote/models/sale_quote.py new file mode 100644 index 00000000000..510c0d1ad70 --- /dev/null +++ b/addons/website_quote/models/sale_quote.py @@ -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) diff --git a/addons/website_quote/quotation_report.xml b/addons/website_quote/report/sale_order_reports.xml similarity index 88% rename from addons/website_quote/quotation_report.xml rename to addons/website_quote/report/sale_order_reports.xml index b9b363b06c6..9b4f6e10082 100644 --- a/addons/website_quote/quotation_report.xml +++ b/addons/website_quote/report/sale_order_reports.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/views/report_saleorder.xml b/addons/website_quote/report/sale_order_templates.xml similarity index 98% rename from addons/website_quote/views/report_saleorder.xml rename to addons/website_quote/report/sale_order_templates.xml index b0bfcbdb5a1..8a6f824ae5c 100644 --- a/addons/website_quote/views/report_saleorder.xml +++ b/addons/website_quote/report/sale_order_templates.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/views/report_quotation.xml b/addons/website_quote/report/website_quote_templates.xml similarity index 93% rename from addons/website_quote/views/report_quotation.xml rename to addons/website_quote/report/website_quote_templates.xml index 55c8ddcc771..db73b677770 100644 --- a/addons/website_quote/views/report_quotation.xml +++ b/addons/website_quote/report/website_quote_templates.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/website_quote/views/sale_order_views.xml b/addons/website_quote/views/sale_order_views.xml new file mode 100644 index 00000000000..4c679196d12 --- /dev/null +++ b/addons/website_quote/views/sale_order_views.xml @@ -0,0 +1,59 @@ + + + + sale.order.form.payment + sale.order + + + +