From 3e5ae006902b3ee813bd1665397f2285bbebd1a9 Mon Sep 17 00:00:00 2001 From: "ajay javiya (OpenERP)" Date: Mon, 2 Dec 2013 18:04:37 +0530 Subject: [PATCH] [ADD]: Quote roller module bzr revid: aja@tinyerp.com-20131202123437-zn6u2ckxbid3jknb --- .../wizard/mail_compose_message.py | 4 +- addons/website_sale_quote/__init__.py | 2 + addons/website_sale_quote/__openerp__.py | 22 ++ .../controllers/__init__.py | 3 + addons/website_sale_quote/controllers/main.py | 57 ++++ addons/website_sale_quote/models/__init__.py | 1 + addons/website_sale_quote/models/order.py | 121 +++++++ addons/website_sale_quote/sale_quote_data.xml | 29 ++ addons/website_sale_quote/sale_quote_view.xml | 17 + .../static/description/icon.png | Bin 0 -> 7439 bytes .../static/src/css/sale_quote.css | 13 + .../static/src/js/sale_quote.js | 32 ++ .../views/website_sale_quote.xml | 322 ++++++++++++++++++ 13 files changed, 620 insertions(+), 3 deletions(-) create mode 100644 addons/website_sale_quote/__init__.py create mode 100644 addons/website_sale_quote/__openerp__.py create mode 100644 addons/website_sale_quote/controllers/__init__.py create mode 100644 addons/website_sale_quote/controllers/main.py create mode 100644 addons/website_sale_quote/models/__init__.py create mode 100644 addons/website_sale_quote/models/order.py create mode 100644 addons/website_sale_quote/sale_quote_data.xml create mode 100644 addons/website_sale_quote/sale_quote_view.xml create mode 100644 addons/website_sale_quote/static/description/icon.png create mode 100644 addons/website_sale_quote/static/src/css/sale_quote.css create mode 100644 addons/website_sale_quote/static/src/js/sale_quote.js create mode 100644 addons/website_sale_quote/views/website_sale_quote.xml diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index ac2f79b56f1..38d08c99b29 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -51,7 +51,7 @@ class mail_compose_message(osv.TransientModel): res.update( self.onchange_template_id( cr, uid, [], context['default_template_id'], res.get('composition_mode'), - res.get('model'), res.get('res_id'), context=context + res.get('model'), res.get('res_id', context.get('active_id')), context=context )['value'] ) return res @@ -114,7 +114,6 @@ class mail_compose_message(osv.TransientModel): values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context)) else: values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context) - if values.get('body_html'): values['body'] = values.pop('body_html') return {'value': values} @@ -169,7 +168,6 @@ class mail_compose_message(osv.TransientModel): # filter template values fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id'] values = dict.fromkeys(res_ids, False) - template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context) for res_id in res_ids: res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field)) diff --git a/addons/website_sale_quote/__init__.py b/addons/website_sale_quote/__init__.py new file mode 100644 index 00000000000..9f86759e32b --- /dev/null +++ b/addons/website_sale_quote/__init__.py @@ -0,0 +1,2 @@ +import controllers +import models diff --git a/addons/website_sale_quote/__openerp__.py b/addons/website_sale_quote/__openerp__.py new file mode 100644 index 00000000000..1db0f379226 --- /dev/null +++ b/addons/website_sale_quote/__openerp__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Quote Roller', + 'category': 'Website', + 'summary': 'Send Live Quotation', + 'version': '1.0', + 'description': """ +OpenERP Sale Quote Roller +================== + + """, + 'author': 'OpenERP SA', + 'depends': ['website_sale','website','product','portal_sale', 'mail'], + 'data': [ + 'views/website_sale_quote.xml', + 'sale_quote_view.xml', + 'sale_quote_data.xml' + ], + 'demo': [ + ], + 'qweb': ['static/src/xml/*.xml'], + 'installable': True, +} diff --git a/addons/website_sale_quote/controllers/__init__.py b/addons/website_sale_quote/controllers/__init__.py new file mode 100644 index 00000000000..e11f9ba81bb --- /dev/null +++ b/addons/website_sale_quote/controllers/__init__.py @@ -0,0 +1,3 @@ +import main + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/website_sale_quote/controllers/main.py b/addons/website_sale_quote/controllers/main.py new file mode 100644 index 00000000000..8f682136902 --- /dev/null +++ b/addons/website_sale_quote/controllers/main.py @@ -0,0 +1,57 @@ +import random +import uuid +import simplejson + +import werkzeug.exceptions + +from openerp import SUPERUSER_ID +from openerp.osv import osv +from openerp.addons.web import http +from openerp.addons.web.http import request +from openerp.addons.website.models import website + +class sale_quote(http.Controller): + + def get_quote(self, token): + quote_pool = request.registry.get('sale.quote') + quote_id = quote_pool.search(request.cr, SUPERUSER_ID, [('access_token', '=', token)], context=request.context) + return quote_id + + @website.route(['/quote/'], type='http', auth="public") + def view(self, token=None, **post): + values = {} + # http://hostname:8069/quote?id= + quote_pool = request.registry.get('sale.quote') + quotation = quote_pool.browse(request.cr, SUPERUSER_ID, self.get_quote(token))[0] + values.update({ + 'quotation' : quotation, + }) + return request.website.render('website_sale_quote.quotation', values) + + @website.route(['/quote//accept'], type='http', auth="public") + def accept(self, token=None , **post): + values = {} + quotation = request.registry.get('sale.quote').write(request.cr, SUPERUSER_ID, self.get_quote(token), {'state': 'accept'}) + return request.redirect("/quote/%s" % token) + + @website.route(['/quote//decline'], type='http', auth="public") + def decline(self, token=None , **post): + values = {} + quotation = request.registry.get('sale.quote').write(request.cr, SUPERUSER_ID, self.get_quote(token), {'state': 'cancel'}) + return request.redirect("/quote/%s" % token) + + @website.route(['/quote//post'], type='http', auth="public") + def post(self, token=None, **post): + values = {} + if post.get('new_message'): + request.session.body = post.get('new_message') + if 'body' in request.session and request.session.body: + request.registry.get('sale.quote').message_post(request.cr, SUPERUSER_ID, self.get_quote(token), + body=request.session.body, + type='comment', + subtype='mt_comment', + ) + request.session.body = False + return request.redirect("/quote/%s" % token) + + diff --git a/addons/website_sale_quote/models/__init__.py b/addons/website_sale_quote/models/__init__.py new file mode 100644 index 00000000000..49178edb61d --- /dev/null +++ b/addons/website_sale_quote/models/__init__.py @@ -0,0 +1 @@ +import order diff --git a/addons/website_sale_quote/models/order.py b/addons/website_sale_quote/models/order.py new file mode 100644 index 00000000000..adca999b588 --- /dev/null +++ b/addons/website_sale_quote/models/order.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import osv, fields +import hashlib +import time + +class sale_quote(osv.Model): + _name = "sale.quote" + _inherit = ['mail.thread', 'ir.needaction_mixin'] + _description = "Sales Quotations" + _columns = { +# 'template_id': fields.many2one('ir.ui.view', 'Template'), + 'order_id': fields.many2one('sale.order', 'Order', required=True), + 'state': fields.selection([ + ('draft', 'Draft Quotation'), + ('sent', 'Quotation Sent'), + ('accept', 'Accept'), + ('cancel', 'Cancelled'), + ('done', 'Done'), + ], 'Status'), + 'to_email': fields.char('Customers Email'), + 'access_token':fields.char('Quotation Token', size=256), + } + + def new_quotation_token(self, cr, uid, record_id): + db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') + quotation_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, record_id)).hexdigest() + return self.write(cr, uid, [record_id],{'access_token': quotation_token} ) + + def create(self, cr, uid, vals, context=None): + if context is None: + context = {} + new_id = super(sale_quote, self).create(cr, uid, vals, context=context) + self.new_quotation_token(cr, uid, new_id) + return new_id + + +class sale_order(osv.osv): + _inherit = 'sale.order' + _columns = { + 'quote_url': fields.char('URL'), + } + +# def action_quotation_send(self, cr, uid, ids, context=None): +# ''' +# This function opens a window to compose an email, with the edi sale template message loaded by default +# ''' +# data_pool = self.pool.get('ir.model.data') +# sale_quote = self.pool.get('sale.quote') +# try: +# compose_form_id = data_pool.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] +# except ValueError: +# compose_form_id = False +# model,template_id = data_pool.get_object_reference(cr, uid, 'website_sale_quote', "email_template_sale_quote") +# ctx = dict(context) +# for order in self.browse(cr, uid, ids, context): +# if not order.quote_id: +# new_id = sale_quote.create(cr, uid,{ +# 'state' : 'draft', +# 'to_email': order.partner_id.email, +# }) +# self.write(cr, uid, [order.id] ,{'quote_id': new_id}, context) +# ctx.update({ +# 'default_model': 'sale.order', +# 'default_res_id': order.id, +# 'default_use_template': bool(template_id), +# 'default_template_id': template_id, +# 'default_composition_mode': 'comment', +# 'mark_so_as_sent': True +# }) +# return { +# 'type': 'ir.actions.act_window', +# 'view_type': 'form', +# 'view_mode': 'form', +# 'res_model': 'mail.compose.message', +# 'views': [(compose_form_id, 'form')], +# 'view_id': compose_form_id, +# 'target': 'new', +# 'context': ctx, +# } + + def action_quotation_send(self, cr, uid, ids, context=None): + quote = super(sale_order, self).action_quotation_send(cr, uid,ids, context) + sale_quote = self.pool.get('sale.quote') + for order in self.browse(cr, uid, ids, context): + q_id = sale_quote.search(cr, uid, [('order_id','=', order.id)], context=context) + if not q_id: + new_id = sale_quote.create(cr, uid,{ + 'order_id' : order.id, + 'state' : 'draft', + 'to_email': order.partner_id.email, + }) + self.write(cr, uid, order.id, {'quote_url': self.get_signup_url(cr, uid, [order.id], context)}) + return quote + + def get_signup_url(self, cr, uid, ids, context=None): + url = False + quote_id = self.pool.get('sale.quote').search(cr, uid, [('order_id','=', ids[0])], context=context) + for quote in self.pool.get('sale.quote').browse(cr, uid, quote_id, context=context): + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context) + url = "%s/quote/%s" % (base_url, quote.access_token) + return url diff --git a/addons/website_sale_quote/sale_quote_data.xml b/addons/website_sale_quote/sale_quote_data.xml new file mode 100644 index 00000000000..ff7c3cfc972 --- /dev/null +++ b/addons/website_sale_quote/sale_quote_data.xml @@ -0,0 +1,29 @@ + + + + + + Sales Quotation - Send by Email (Quote) + ${object.user_id.email or ''} + ${object.company_id.name} ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} (Ref ${object.name or 'n/a' }) + ${object.partner_invoice_id.id} + + + + +

Hello ${object.partner_id.name},

+ +

Here is your ${object.state in ('draft', 'sent') and 'quotation' or 'order confirmation'} from ${object.company_id.name}:

+ + <% set signup_url = object.get_quote_url() %> +

+ You can access this document and pay online via our Customer Portal: +

+ View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} + + ]]>
+
+
+
diff --git a/addons/website_sale_quote/sale_quote_view.xml b/addons/website_sale_quote/sale_quote_view.xml new file mode 100644 index 00000000000..b5120ace33a --- /dev/null +++ b/addons/website_sale_quote/sale_quote_view.xml @@ -0,0 +1,17 @@ + + + + + + + sale.order.form.payment + sale.order + + + + + + + + + diff --git a/addons/website_sale_quote/static/description/icon.png b/addons/website_sale_quote/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b9f4f25bd009970ad1f6bb2823c1b8593ad2bd GIT binary patch literal 7439 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 + + + + + +