diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..79069af38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# IDE +.idea + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + diff --git a/INCOMPLETE/membership_subscription/__init__.py b/INCOMPLETE/membership_subscription/__init__.py new file mode 100644 index 000000000..e89927ca6 --- /dev/null +++ b/INCOMPLETE/membership_subscription/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/__manifest__.py b/INCOMPLETE/membership_subscription/__manifest__.py new file mode 100644 index 000000000..fbe092866 --- /dev/null +++ b/INCOMPLETE/membership_subscription/__manifest__.py @@ -0,0 +1,27 @@ +{ + 'name': "Membership Subscription", + 'version': "1.0.0", + 'author': "Sythil Tech", + 'category': "Tools", + 'support': "steven@sythiltech.com.au", + 'summary':'Create membership programs that automatically manage subscriptions using payment tokens', + 'description':'Create membership programs that automatically manage subscriptions using payment tokens', + 'license':'LGPL-3', + 'data': [ + 'security/ir.model.access.csv', + 'security/ir_rule.xml', + 'data/website.menu.csv', + 'views/payment_membership_views.xml', + 'views/membership_subscription_templates.xml', + ], + 'depends': ['payment_reoccuring'], + 'demo': [ + 'demo/ir.module.category.csv', + 'demo/res.groups.xml', + 'demo/payment_membership.xml', + ], + 'images':[ + 'static/description/1.jpg', + ], + 'installable': True, +} \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/controllers/__init__.py b/INCOMPLETE/membership_subscription/controllers/__init__.py new file mode 100644 index 000000000..6920e2020 --- /dev/null +++ b/INCOMPLETE/membership_subscription/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import main \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/controllers/main.py b/INCOMPLETE/membership_subscription/controllers/main.py new file mode 100644 index 000000000..4aac04280 --- /dev/null +++ b/INCOMPLETE/membership_subscription/controllers/main.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +import logging +_logger = logging.getLogger(__name__) +import werkzeug +import requests +import json + +import odoo.http as http +from odoo.http import request +from odoo.addons.http_routing.models.ir_http import slug + +class MembershipSubscriptionController(http.Controller): + + @http.route('/membership/form/load', website=True, type='json', auth="user") + def membership_form_load(self, **kw): + + values = {} + for field_name, field_value in kw.items(): + values[field_name] = field_value + + membership_form = request.env['payment.membership'].browse(int(values['form_id'])) + + return {'form_id': membership_form.id, 'payment_acquirer': membership_form.subscription_id.payment_acquirer_id.id} + + @http.route('/membership/cancel', type="http", auth="user", website=True) + def membership_cancel(self): + + #Remove the membership assignment + request.env.user.partner_id.payment_membership_id = False + + #Remove all permissions + request.env.user.groups_id = False + + #Also add them to the portal group so they can access the website + group_portal = request.env['ir.model.data'].sudo().get_object('base','group_portal') + group_portal.users = [(4, request.env.user.id)] + + return werkzeug.utils.redirect("/") + + @http.route('/membership/management', type="http", auth="user", website=True) + def membership_management(self): + return request.render('membership_subscription.membership_management', {'current_membership': request.env.user.partner_id.payment_membership_id}) + + @http.route('/membership/signup/thank-you', type="http", auth="user", website=True) + def membership_signup_thankyou(self): + return request.render('membership_subscription.signup_thank_you', {}) + + @http.route('/membership/signup/', type="http", auth="public", website=True) + def membership_signup(self, membership): + return request.render('membership_subscription.signup_form', {'membership': membership}) + + @http.route('/membership/signup/process', type="http", auth="public", website=True) + def membership_signup_process(self, **kwargs): + + values = {} + for field_name, field_value in kwargs.items(): + values[field_name] = field_value + + membership = request.env['payment.membership'].sudo().browse( int( values['membership_id'] ) ) + + # The user account is created now but access right are only given when payment has been received + if request.env['res.users'].sudo().search_count([('login', '=', values['email'])]) == 0: + new_user = request.env['res.users'].sudo().create({'name': values['name'], 'login': values['email'], 'email': values['email'], 'password': values['password'] }) + else: + return "User account with this login already exists" + + # Modify the users partner record only with the allowed fields + extra_fields_dict = {} + for extra_field in membership.extra_field_ids: + extra_fields_dict[extra_field.sudo().field_id.name] = values[extra_field.name] + + new_user.partner_id.write(extra_fields_dict) + + #Remove all permissions + new_user.groups_id = False + + #Also add them to the portal group so they can access the website + group_portal = request.env['ir.model.data'].sudo().get_object('base','group_portal') + group_portal.users = [(4, new_user.id)] + + # Add them to the membership + extra_fields_dict['payment_membership_id'] = membership.id + + # Paid memberships get redirected to the payment gateway assigned to the subscription + if membership.subscription_id: + #Add the subscription product to the order + order = request.website.sale_get_order(force_create=1) + request.website.sale_reset() + order._cart_update(product_id=membership.subscription_id.product_id.id, set_qty=1) + + return json.JSONEncoder().encode({'status': 'unpaid', 'acquirer': membership.subscription_id.payment_acquirer_id.id}) + else: + + # Users of free membership gain instant rights access + for user_group in membership.group_ids: + user_group.users = [(4, new_user.id)] + + # Automatically sign the new user in + request.cr.commit() # as authenticate will use its own cursor we need to commit the current transaction + request.session.authenticate(request.env.cr.dbname, values['email'], values['password']) + + # Redirect them to the thank you page + return json.JSONEncoder().encode({'status': 'paid', 'redirect_url': membership.redirect_url}) \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/data/website.menu.csv b/INCOMPLETE/membership_subscription/data/website.menu.csv new file mode 100644 index 000000000..5f19b693f --- /dev/null +++ b/INCOMPLETE/membership_subscription/data/website.menu.csv @@ -0,0 +1,2 @@ +"id","name","url","parent_id/id" +"membership_website_menu","Membership","/membership/management","website.main_menu" diff --git a/INCOMPLETE/membership_subscription/demo/ir.module.category.csv b/INCOMPLETE/membership_subscription/demo/ir.module.category.csv new file mode 100644 index 000000000..63282e93b --- /dev/null +++ b/INCOMPLETE/membership_subscription/demo/ir.module.category.csv @@ -0,0 +1,2 @@ +"id","name" +"demo_membership","Membership Levels" \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/demo/payment_membership.xml b/INCOMPLETE/membership_subscription/demo/payment_membership.xml new file mode 100644 index 000000000..3b69a4eb3 --- /dev/null +++ b/INCOMPLETE/membership_subscription/demo/payment_membership.xml @@ -0,0 +1,19 @@ + + + + + Bronze + + + + + Silver + + + + + Gold + + + + \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/demo/res.groups.xml b/INCOMPLETE/membership_subscription/demo/res.groups.xml new file mode 100644 index 000000000..c620cef51 --- /dev/null +++ b/INCOMPLETE/membership_subscription/demo/res.groups.xml @@ -0,0 +1,26 @@ + + + + + + Bronze + + Teir 1 Membership + + + + Silver + + + Teir 2 Membership + + + + Gold + + + Teir 3 Membership + + + + \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/doc/changelog.rst b/INCOMPLETE/membership_subscription/doc/changelog.rst new file mode 100644 index 000000000..f160755b1 --- /dev/null +++ b/INCOMPLETE/membership_subscription/doc/changelog.rst @@ -0,0 +1,3 @@ +v1.0.0 +====== +* Initial Release \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/models/__init__.py b/INCOMPLETE/membership_subscription/models/__init__.py new file mode 100644 index 000000000..41304bfab --- /dev/null +++ b/INCOMPLETE/membership_subscription/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import payment_membership +from . import res_partner \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/models/payment_membership.py b/INCOMPLETE/membership_subscription/models/payment_membership.py new file mode 100644 index 000000000..4287c52d8 --- /dev/null +++ b/INCOMPLETE/membership_subscription/models/payment_membership.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools + +class PaymentMembership(models.Model): + + _name = "payment.membership" + + name = fields.Char(string="Name", required=True) + subscription_id = fields.Many2one('payment.subscription', string="Subscription") + redirect_url = fields.Char(string="Redirect URL", default="/membership/signup/thank-you") + group_ids = fields.Many2many('res.groups', string="New User Groups", help="Determines what the user can access e.g. slide channels / support help groups") + extra_field_ids = fields.One2many('payment.membership.field', 'payment_membership_id', string="Extra Fields", help="More fields will appear on the website signup form") + member_ids = fields.One2many('res.partner', 'payment_membership_id', string="Members") + + @api.multi + def view_singup_form(self): + self.ensure_one() + + return { + 'type': 'ir.actions.act_url', + 'name': "Membership Signup Form", + 'target': 'self', + 'url': "/membership/signup/" + slug(self) + } + +class PaymentMembershipField(models.Model): + + _name = "payment.membership.field" + + payment_membership_id = fields.Many2one('payment.membership', string="Payment Membership") + field_id = fields.Many2one('ir.model.fields', string="Field", domain="[('model_id.model','=','res.partner')]", required=True) + name = fields.Char(string="Name") + field_type = fields.Selection([('textbox','Textbox')], string="Field Type", default="textbox", required=True) + field_label = fields.Char(string="Field Label", required=True) + + @api.onchange('field_id') + def _onchange_field_id(self): + if self.field_id: + self.field_label = self.field_id.field_description + self.name = self.field_id.name \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/models/res_partner.py b/INCOMPLETE/membership_subscription/models/res_partner.py new file mode 100644 index 000000000..92d1fa6e9 --- /dev/null +++ b/INCOMPLETE/membership_subscription/models/res_partner.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools + +class RespartnerMembership(models.Model): + + _inherit = "res.partner" + + payment_membership_id = fields.Many2one('payment.membership', string="Membership") \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/security/ir.model.access.csv b/INCOMPLETE/membership_subscription/security/ir.model.access.csv new file mode 100644 index 000000000..20ded7200 --- /dev/null +++ b/INCOMPLETE/membership_subscription/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_payment_membership","access payment.membership","model_payment_membership","sales_team.group_sale_manager",1,1,1,1 +"public_access_payment_membership","public access payment.membership","model_payment_membership","",1,0,0,0 +"access_payment_membership_field","access payment.membership.field","model_payment_membership_field","sales_team.group_sale_manager",1,1,1,1 +"public_access_payment_membership_field","public access payment.membership.field","model_payment_membership_field","",1,0,0,0 \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/security/ir_rule.xml b/INCOMPLETE/membership_subscription/security/ir_rule.xml new file mode 100644 index 000000000..123d057f0 --- /dev/null +++ b/INCOMPLETE/membership_subscription/security/ir_rule.xml @@ -0,0 +1,14 @@ + + + + + + Website Membership Menu Access + + [('url','!=', '/membership/management')] + + + + + + diff --git a/INCOMPLETE/membership_subscription/static/description/index.html b/INCOMPLETE/membership_subscription/static/description/index.html new file mode 100644 index 000000000..aadd7274d --- /dev/null +++ b/INCOMPLETE/membership_subscription/static/description/index.html @@ -0,0 +1,6 @@ +
+

Description

+Create membership programs that automatically manage subscriptions using payment tokens
+
+Find a bug or need support? send an email to steven@sythiltech.com.au
+
\ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/static/src/img/ui/snippet_thumb_membership_form.jpg b/INCOMPLETE/membership_subscription/static/src/img/ui/snippet_thumb_membership_form.jpg new file mode 100644 index 000000000..f8deea2ab Binary files /dev/null and b/INCOMPLETE/membership_subscription/static/src/img/ui/snippet_thumb_membership_form.jpg differ diff --git a/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.editor.js b/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.editor.js new file mode 100644 index 000000000..f15ecb7d5 --- /dev/null +++ b/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.editor.js @@ -0,0 +1,52 @@ +odoo.define('membership.form.editor', function (require) { +'use strict'; + +var base = require('web_editor.base'); +var options = require('web_editor.snippets.options'); +var core = require('web.core'); +var session = require('web.session'); +var website = require('website.website'); +var ajax = require('web.ajax'); +var qweb = core.qweb; +var wUtils = require('website.utils'); +var rpc = require('web.rpc'); +var weContext = require('web_editor.context'); + +options.registry.membership_form = options.Class.extend({ + onBuilt: function() { + var self = this; + + rpc.query({ + model: 'payment.membership', + method: 'name_search', + args: [], + context: weContext.get() + }).then(function(form_ids){ + + wUtils.prompt({ + id: "editor_new_membership_form", + window_title: "Choose Membership Form", + select: "Select Form", + init: function (field) { + return form_ids; + }, + }).then(function (form_id) { + + session.rpc('/membership/form/load', {'form_id': form_id}).then(function(result) { + self.$target.find("input[name='membership_id']").val(result.form_id); + }); + }); + + }); + + }, + cleanForSave: function () { + var self = this; + //Sometimes the form gets saved with the token in it + self.$target.find("input[name='csrf_token']").removeAttr("value"); + self.$target.find("input[name='csrf_token']").attr("t-att-value","request.csrf_token()"); + }, + +}); + +}); \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.js b/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.js new file mode 100644 index 000000000..f42c68139 --- /dev/null +++ b/INCOMPLETE/membership_subscription/static/src/js/membership.snippets.js @@ -0,0 +1,68 @@ +odoo.define('membership.form.front', function (require) { +'use strict'; + +var base = require('web_editor.base'); +var core = require('web.core'); +var session = require('web.session'); +var website = require('website.website'); +var ajax = require('web.ajax'); +var qweb = core.qweb; +var wUtils = require('website.utils'); +var rpc = require('web.rpc'); +var weContext = require('web_editor.context'); + +$(function() { + + $(".btn-website-membership-signup").click(function(e) { + + e.preventDefault(); // Prevent the default submit behavior + + var my_form = $("form"); + + // Prepare form inputs + var form_data = my_form.serializeArray(); + + var form_values = {}; + _.each(form_data, function(input) { + if (input.value != '') { + form_values[input.name] = input.value; + } + }); + + ajax.post('/membership/signup/process', form_values).then(function(result) { + if (result) { + var result_data = $.parseJSON(result); + if (result_data.status == "paid") { + window.location = result_data.redirect_url; + } else if (result_data.status == "unpaid") { + ajax.jsonRpc('/shop/payment/transaction', 'call', { + 'acquirer_id': result_data.acquirer, + 'save_token': true, + }).then(function (result) { + if (result) { + // if the server sent us the html form, we create a form element + var newForm = document.createElement('form'); + newForm.setAttribute("method", "post"); // set it to post + //newForm.setAttribute("provider", checked_radio.dataset.provider); + newForm.hidden = true; // hide it + newForm.innerHTML = result; // put the html sent by the server inside the form + var action_url = $(newForm).find('input[name="data_set"]').data('actionUrl'); + newForm.setAttribute("action", action_url); // set the action url + $(document.getElementsByTagName('body')[0]).append(newForm); // append the form to the body + $(newForm).find('input[data-remove-me]').remove(); // remove all the input that should be removed + if(action_url) { + newForm.submit(); // and finally submit the form + } + } else { + alert("We are not able to redirect you to the payment form."); + } + }).fail(function (message, data) { + alert("We are not able to redirect you to the payment form.

" + (core.debug ? data.data.message : '')); + }); + } + } + }); + }); +}); + +}); \ No newline at end of file diff --git a/INCOMPLETE/membership_subscription/views/membership_subscription_templates.xml b/INCOMPLETE/membership_subscription/views/membership_subscription_templates.xml new file mode 100644 index 000000000..adc804ed4 --- /dev/null +++ b/INCOMPLETE/membership_subscription/views/membership_subscription_templates.xml @@ -0,0 +1,131 @@ + + + + + + + + + + \ No newline at end of file diff --git a/OTHER/voip_sip_webrtc_twilio_self/views/voip_twilio_invoice_views.xml b/OTHER/voip_sip_webrtc_twilio_self/views/voip_twilio_invoice_views.xml new file mode 100644 index 000000000..b330b5074 --- /dev/null +++ b/OTHER/voip_sip_webrtc_twilio_self/views/voip_twilio_invoice_views.xml @@ -0,0 +1,29 @@ + + + + + + + voip.twilio.invoice form view + voip.twilio.invoice + +
+ + + + + + + + + + + + + + + + + +
+ + + voip.twilio tree view + voip.twilio + + + + + + + + + Twilio Accounts + voip.twilio + tree,form + +

+ No Twilio Accounts +

+
+
+ +
\ No newline at end of file diff --git a/app_store/__init__.py b/app_store/__init__.py new file mode 100644 index 000000000..e89927ca6 --- /dev/null +++ b/app_store/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers \ No newline at end of file diff --git a/app_store/__manifest__.py b/app_store/__manifest__.py new file mode 100644 index 000000000..308322f18 --- /dev/null +++ b/app_store/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': "Custom App Store", + 'version': "1.1.7", + 'author': "Sythil Tech", + 'category': "Tools", + 'summary': "Create your own app store", + 'license':'LGPL-3', + 'data': [ + 'views/module_overview_templates.xml', + 'views/module_overview_views.xml', + 'views/appstore_account_views.xml', + 'views/module_access_views.xml', + 'views/menus.xml', + 'data/website.menu.csv', + 'data/ir.cron.csv', + 'security/ir.model.access.csv', + ], + 'demo': [], + 'depends': ['website'], + 'images':[ + 'static/description/1.jpg', + ], + 'installable': True, +} \ No newline at end of file diff --git a/app_store/controllers/__init__.py b/app_store/controllers/__init__.py new file mode 100644 index 000000000..6920e2020 --- /dev/null +++ b/app_store/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import main \ No newline at end of file diff --git a/app_store/controllers/main.py b/app_store/controllers/main.py new file mode 100644 index 000000000..b83320daf --- /dev/null +++ b/app_store/controllers/main.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +import base64 +import requests +import urllib +import os +from os.path import expanduser +from datetime import datetime +from lxml import html, etree +import logging +_logger = logging.getLogger(__name__) +import zipfile +import json +import io +import csv +import werkzeug.utils +import werkzeug.wrappers +import logging +_logger = logging.getLogger(__name__) + +from openerp.tools import ustr +import openerp.http as http +from openerp.http import request + +class AppsController(http.Controller): + + @http.route('/client/apps', type="http", auth="public", website=True) + def client_browse_apps(self, **kwargs): + """Browse all the modules inside Odoo client""" + + values = {} + for field_name, field_value in kwargs.items(): + values[field_name] = field_value + + if 'acess_token' in values: + modules = request.env['module.overview'].search(['|', ('published', '=', True), ('private_access_ids.access_token', '=', values['acess_token'])]) + else: + modules = request.env['module.overview'].search([('published', '=', True)]) + + return http.request.render('app_store.client_app_list', {'modules':modules}) + + @http.route('/apps', type="http", auth="public", website=True) + def browse_apps(self, **kwargs): + """Browse all the modules""" + + values = {} + for field_name, field_value in kwargs.items(): + values[field_name] = field_value + + modules = request.env['module.overview'].search([]) + return http.request.render('app_store.app_list', {'modules':modules}) + + @http.route('/apps/modules/download/', type="http", auth="public") + def app_download(self, module_name, **kwargs): + """Download the module zip""" + + filename = module_name + ".zip" + headers = [ + ('Content-Type', 'application/octet-stream; charset=binary'), + ('Content-Disposition', "attachment; filename=" + filename ), + ] + + module = request.env['module.overview'].sudo().search([('name', '=', module_name)]) + module.module_download_count += 1 + + home_directory = os.path.expanduser('~') + app_directory = home_directory + "/apps" + + response = werkzeug.wrappers.Response(open(app_directory + "/" + module_name + ".zip", mode="r+b"), headers=headers, direct_passthrough=True) + return response + + def content_disposition(self, filename): + filename = ustr(filename) + + #escaped = urllib2.quote(filename.encode('utf8')) + + browser = request.httprequest.user_agent.browser + version = int((request.httprequest.user_agent.version or '0').split('.')[0]) + if browser == 'msie' and version < 9: + return "attachment; filename=%s" % escaped + elif browser == 'safari' and version < 537: + return u"attachment; filename=%s" % filename.encode('ascii', 'replace') + else: + return "attachment; filename*=UTF-8''%s" % escaped + + @http.route('/apps/modules/', type="http", auth="public", website=True) + def app_page(self, module_name, **kwargs): + """View all the details about a module""" + + values = {} + for field_name, field_value in kwargs.items(): + values[field_name] = field_value + + module = request.env['module.overview'].search([('name','=',module_name)]) + + if module.published == False: + return "No hack bypassing published" + + module.sudo().module_view_count += 1 + + header_string = "" + for keys,values in request.httprequest.headers.items(): + header_string += keys + ": " + values + "\n" + + ref = "" + if "Referer" in request.httprequest.headers: + ref = request.httprequest.headers['Referer'] + + request.env['module.overview.store.view'].sudo().create({'mo_id': module.id, 'ref':ref, 'ip': request.httprequest.remote_addr,'header':header_string}) + + return http.request.render('app_store.app_page', {'overview':module}) + + @http.route('/client/apps/modules/', type="http", auth="public", website=True) + def app_page_client(self, module_name, **kwargs): + """View all the details about a module""" + + values = {} + for field_name, field_value in kwargs.items(): + values[field_name] = field_value + + module = request.env['module.overview'].search([('name','=',module_name)]) + + if module.published == False: + return "No hack bypassing published" + + module.module_view_count += 1 + + header_string = "" + for keys,values in request.httprequest.headers.items(): + header_string += keys + ": " + values + "\n" + + ref = "" + if "Referer" in request.httprequest.headers: + ref = request.httprequest.headers['Referer'] + + request.env['module.overview.store.view'].create({'mo_id': module.id, 'ref':ref, 'ip': request.httprequest.remote_addr,'header':header_string}) + + return http.request.render('app_store.client_app_page', {'overview':module}) + + @http.route('/custom/store/updates', type="http", auth="public") + def custom_app_store_updates(self, **kwargs): + module_list = [] + for md in request.env['module.overview'].search([('version', '!=', False), ('published', '=', True)]): + if md.version.startswith("11.0."): + module_list.append({'name': md.name, 'latest_version': md.version}) + else: + #Prefix with 11.0 so we can compare against the installed version + module_list.append({'name': md.name, 'latest_version': "11.0." + md.version}) + + return json.dumps(module_list) \ No newline at end of file diff --git a/app_store/data/ir.cron.csv b/app_store/data/ir.cron.csv new file mode 100644 index 000000000..95d460a14 --- /dev/null +++ b/app_store/data/ir.cron.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,state,code,interval_number,interval_type,numbercall,active +"appstore_check","App Store Repository Sync",model_appstore_account_repository,"code",model.check_all_repositories(),12,hours,-1,true diff --git a/app_store/data/website.menu.csv b/app_store/data/website.menu.csv new file mode 100644 index 000000000..744af8c26 --- /dev/null +++ b/app_store/data/website.menu.csv @@ -0,0 +1,2 @@ +"id","name","url","parent_id/id" +"app_store","App Store","/apps","website.main_menu" diff --git a/app_store/doc/changelog.rst b/app_store/doc/changelog.rst new file mode 100644 index 000000000..3c743fbdf --- /dev/null +++ b/app_store/doc/changelog.rst @@ -0,0 +1,76 @@ +v1.1.7 +====== +* Unpublished modules can not be updated + +v1.1.6 +====== +* Skips analyses of view/models if an error is encountered + +v1.1.5 +====== +* Fix code syntax error + +v1.1.4 +====== +* Skip views without names + +v1.1.3 +====== +* Add a missing permission + +v1.1.2 +====== +* Stop updates from breaking if a module lacks a proper version number + +v1.1.1 +====== +* Bug fix for systems with old version of app_store client without access_token + +v1.1.0 +====== +* Private access to unpublished modules (alpha/beta access) + +v1.0.9 +====== +* Skip repos on error (better error handling later...) + +v1.0.8 +====== +* Tidy up repos on delete of account + +v1.0.7 +====== +* Public access security fixes + +v1.0.6 +====== +* Ability to unpublish modules from the store + +v1.0.5 +====== +* Added summary field + +v1.0.4 +====== +* Fix analyse issue +* Addd author field +* Revamp user interface of client app store +* Fix latest version number display in custom apps update + +v1.0.3 +====== +* Fix to not analyse modules with no version number +* Fix for modules whose version number already starts with 11.0 +* Fix for github repo zips not being in root ~/apps directory which update mechanism expects + +v1.0.2 +====== +* Module update support and github sync fix + +v1.0.1 +====== +* Fix module analyse issue + +v1.0.0 +====== +* Port to version 11 \ No newline at end of file diff --git a/app_store/models/__init__.py b/app_store/models/__init__.py new file mode 100644 index 000000000..8d37e493b --- /dev/null +++ b/app_store/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import module_overview +from . import appstore_account \ No newline at end of file diff --git a/app_store/models/appstore_account.py b/app_store/models/appstore_account.py new file mode 100644 index 000000000..7a9977b6d --- /dev/null +++ b/app_store/models/appstore_account.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -* +import io +import zipfile +import tempfile +from urllib.request import Request, urlopen +import os +from io import BytesIO +import sys +from os import walk +import glob +from lxml import html, etree +import csv +import fnmatch +import logging +_logger = logging.getLogger(__name__) +import ast +import base64 +import os.path +from docutils.core import publish_string +import json + +from odoo import api, fields, models + +class AppstoreAccount(models.Model): + + _name = "appstore.account" + _description = "App Store Account" + + name = fields.Char(string="Name") + repositories_ids = fields.One2many('appstore.account.repository', 'asa_id', string="Repositories") + + def sync_repos(self): + self.env['appstore.account.repository'].check_all_repositories() + +class AppstoreAccountRepository(models.Model): + + _name = "appstore.account.repository" + _description = "App Store Account Repository" + + asa_id = fields.Many2one('appstore.account', ondelete='cascade', string="App Store Account") + url = fields.Char(string="Repository URL") + token = fields.Char(String="Token") + + @api.model + def check_all_repositories(self): + """Checks to see if there are any new modules""" + + filename = tempfile.mktemp('.zip') + destDir = tempfile.mktemp() + home_directory = os.path.expanduser('~') + app_directory = home_directory + "/apps" + + for account_repository in self.env['appstore.account.repository'].search([]): + rep_directory = app_directory + "/" + account_repository.url.split("/")[3] + + repository_url = account_repository.url.split("#")[0] + "/archive/" + account_repository.url.split("#")[1] + ".zip" + q = Request(repository_url) + + if account_repository.token: + q.add_header('Authorization', 'token ' + account_repository.token) + + try: + repo_data = urlopen(q).read() + except: + _logger.error("Failed to read repo") + continue + + thefile = zipfile.ZipFile(BytesIO(repo_data)) + + if not os.path.exists(rep_directory): + os.makedirs(rep_directory) + + thefile.extractall(rep_directory) + + thefile.close() + + rep_name = account_repository.url.split("/")[4].replace("#","-") + + full_rep_path = rep_directory + "/" + rep_name + + #Go through all module folders under the repository directory and analyse the module + for dir in os.listdir(full_rep_path): + if os.path.isdir(os.path.join(full_rep_path, dir)): + self.analyse_module(dir, full_rep_path) + + def analyse_module(self, module_name, app_directory): + try: + manifest_file = "" + if os.path.exists(app_directory + "/" + module_name + "/__manifest__.py"): + manifest_file = "__manifest__.py" + + if os.path.exists(app_directory + "/" + module_name + "/__openerp__.py"): + manifest_file = "__openerp__.py" + + if manifest_file == "": + #If they module does not have a manifest file do not even bother + return 0 + + with open(app_directory + "/" + module_name + "/" + manifest_file, 'r') as myfile: + #Remove comments as literal_eval hates them + trimmed_data = "" + for i, line in enumerate(myfile): + if not line.lstrip().startswith("#"): + trimmed_data += line + + #trimmed_data = trimmed_data.replace("'", "\"") + op_settings = ast.literal_eval(trimmed_data) + + + #Modules that don't have version number are not analysed + if 'version' not in op_settings: + return 0 + + #Convert icon file to base64 + icon_base64 = "" + if os.path.isfile(app_directory + "/" + module_name + "/static/description/icon.png"): + with open(app_directory + "/" + module_name + "/static/description/icon.png", "rb") as image_file: + icon_base64 = base64.b64encode(image_file.read()) + + if self.env['module.overview'].search_count([('name', '=', module_name)]) == 0: + module_overview = self.env['module.overview'].create({'name': module_name}) + else: + #Clear out most things except download / view count + module_overview = self.env['module.overview'].search([('name', '=', module_name)])[0] + module_overview.models_ids.unlink() + module_overview.menu_ids.unlink() + module_overview.group_ids.unlink() + module_overview.image_ids.unlink() + module_overview.depend_ids.unlink() + + if 'author' in op_settings: + module_overview.author = op_settings['author'] + + if 'summary' in op_settings: + module_overview.summary = op_settings['summary'] + + module_overview.module_name = op_settings['name'] + module_overview.icon = icon_base64 + module_overview.version = op_settings['version'] + + except Exception as e: + _logger.error(module_name) + _logger.error(e) + exc_type, exc_obj, exc_tb = sys.exc_info() + _logger.error("Line: " + str(exc_tb.tb_lineno) ) + + try: + #Read /doc/changelog.rst file + if os.path.exists(app_directory + "/" + module_name + "/doc/changelog.rst"): + with open(app_directory + "/" + module_name + "/doc/changelog.rst", 'r') as changelogfile: + changelogdata = changelogfile.read() + module_overview.change_log_raw = changelogdata + module_overview.change_log_html = changelogdata + #module_overview.change_log_html = publish_string(changelogdata, writer_name='html').split("\n",2)[2] + + #Read /static/description/index.html file + if os.path.exists(app_directory + "/" + module_name + "/static/description/index.html"): + with open(app_directory + "/" + module_name + "/static/description/index.html", 'r') as descriptionfile: + descriptiondata = descriptionfile.read() + module_overview.store_description = descriptiondata + + if 'depends' in op_settings: + for depend in op_settings['depends']: + self.env['module.overview.depend'].create({'mo_id': module_overview.id, 'name': depend}) + + if 'images' in op_settings: + for img in op_settings['images']: + image_path = app_directory + "/" + module_name + "/" + img + if os.path.exists(image_path): + with open(image_path, "rb") as screenshot_file: + screenshot_base64 = base64.b64encode(screenshot_file.read()) + + self.env['module.overview.image'].create({'mo_id': module_overview.id, 'name': img, 'file_data': screenshot_base64}) + + for root, dirnames, filenames in os.walk(app_directory + '/' + module_name): + for filename in fnmatch.filter(filenames, '*.xml'): + self._read_xml(os.path.join(root, filename), module_overview.id) + + #for filename in fnmatch.filter(filenames, '*.csv'): + # self._read_csv(filename, open( os.path.join(root, filename) ).read(), module_overview.id) + + except Exception as e: + _logger.error(module_name) + _logger.error(e) + exc_type, exc_obj, exc_tb = sys.exc_info() + _logger.error("Line: " + str(exc_tb.tb_lineno) ) + pass + + #Create a zip of the module (TODO include dependacies) + zf = zipfile.ZipFile(os.path.expanduser('~') + "/apps/" + module_name + ".zip", "w") + for dirname, subdirs, files in os.walk(app_directory + "/" + module_name): + for filename in files: + full_file_path = os.path.join(dirname, filename) + zf.write(full_file_path, arcname=os.path.relpath(full_file_path, app_directory + "/" + module_name)) + zf.close() + + def _read_csv(self, file_name, file_content, m_id): + if "ir.model.access": + rownum = 0 + reader = csv.reader(file_content, delimiter=',') + header = [] + for row in reader: + row_dict = {'mo_id': m_id} + # Save header row. + if rownum == 0: + header = row + else: + colnum = 0 + for col in row: + #map the csv header columns to the fields + if header[colnum] == "id": + row_dict['x_id'] = col + elif header[colnum] == "model_id:id": + row_dict['model'] = col + elif header[colnum] == "group_id:id": + row_dict['group'] = col + elif header[colnum] == "perm_read": + row_dict['perm_read'] = bool(int(col)) + elif header[colnum] == "perm_write": + row_dict['perm_write'] = bool(int(col)) + elif header[colnum] == "perm_create": + row_dict['perm_create'] = bool(int(col)) + elif header[colnum] == "perm_unlink": + row_dict['perm_unlink'] = bool(int(col)) + else: + row_dict[header[colnum]] = col + + colnum += 1 + + group_name = "" + + #Deal with blank or other malformed rows + if ('group' not in row_dict) or ('x_id' not in row_dict): + continue + + if row_dict['group'] != "": + group_name = self.env['ir.model.data'].get_object(row_dict['group'].split(".")[0], row_dict['group'].split(".")[1]).display_name + + #Create the group if it does exist + ex_group = self.env['module.overview.group'].search([('x_id','=',row_dict['group'])]) + + if len(ex_group) == 0: + my_group = self.env['module.overview.group'].create({'mo_id': m_id, 'name': group_name, 'x_id': row_dict['group']}) + else: + my_group = ex_group[0] + + #If model diiesn't exists, create it + ex_model = self.env['module.overview.model'].search([('name','=', self._ref_to_model(row_dict['model']) )]) + + if len(ex_model) == 0: + #create the model + my_model = self.env['module.overview.model'].create({'name':self._ref_to_model(row_dict['model']), 'mo_id': m_id}) + else: + my_model = ex_model[0] + + #Add the access rule to the group + self.env['module.overview.group.access'].create({'mog_id':my_group.id, 'model_id': my_model.id, 'perm_read': row_dict['perm_read'], 'perm_write':row_dict['perm_write'], 'perm_create':row_dict['perm_create'], 'perm_unlink':row_dict['perm_unlink']}) + + access_dict = {'model_id':my_model.id, 'name': group_name, 'x_id': row_dict['group'], 'perm_read': row_dict['perm_read'], 'perm_write':row_dict['perm_write'], 'perm_create':row_dict['perm_create'], 'perm_unlink':row_dict['perm_unlink']} + self.env['module.overview.model.access'].create(access_dict) + + rownum += 1 + + def _ref_to_model(self, ref_string): + """Turns 'model_sms_message' into 'sms.message'""" + return ref_string.replace("model_","").replace("_",".") + + def _read_xml(self, file_path, m_id): + return_string = "" + root = etree.parse(file_path) + + insert_menu = root.xpath('//menuitem') + for menu in insert_menu: + menu_dict = {'mo_id': m_id} + menu_dict['x_id'] = menu.attrib['id'] + + if 'name' in menu.attrib: + menu_dict['name'] = menu.attrib['name'] + + if 'parent' in menu.attrib: + try: + menu_dict['parent'] = self.env['ir.model.data'].get_object(menu.attrib['parent'].split(".")[0], menu.attrib['parent'].split(".")[1]).display_name + except: + pass + menu_dict['parent_x_id'] = menu.attrib['parent'] + + self.env['module.overview.menu'].create(menu_dict) + + insert_records = root.xpath('//record') + for rec in insert_records: + record_id = rec.attrib['id'] + + #If it's a view + if rec.attrib['model'] == "ir.ui.view": + if len(rec.find(".//field[@name='name']")) > 0: + record_name = rec.find(".//field[@name='name']").text + else: + continue + + model_name = rec.find(".//field[@name='model']").text + model_exist = self.env['module.overview.model'].search([('name','=',model_name),('mo_id','=',m_id) ]) + model = "" + + #if this is the first time encountering model, create it. + if len(model_exist) == 0: + model = self.env['module.overview.model'].create({'mo_id': m_id, 'name': model_name}) + else: + model = model_exist[0] + + #add this view to this model + self.env['module.overview.model.view'].create({'model_id': model.id, 'name': record_name, 'x_id': record_id}) \ No newline at end of file diff --git a/app_store/models/module_overview.py b/app_store/models/module_overview.py new file mode 100644 index 000000000..131e59732 --- /dev/null +++ b/app_store/models/module_overview.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -* +import os +from os import walk +import glob +from lxml import html, etree +import csv +import fnmatch +import zipfile +import logging +import ast +import base64 +_logger = logging.getLogger(__name__) +import os.path +from docutils.core import publish_string +import sys +import json + +from openerp import api, fields, models + +class ModuleOverview(models.Model): + + _name = "module.overview" + _description = "Module Overview" + + name = fields.Char(string="Internal Name") + private_access_ids = fields.Many2many('module.access', string="Private Access") + models_ids = fields.One2many('module.overview.model', 'mo_id', string="Models") + model_count = fields.Integer(string="Model Count", compute="_compute_model_count") + menu_ids = fields.One2many('module.overview.menu', 'mo_id', string="Menus") + menu_count = fields.Integer(string="Menu Count", compute="_compute_menu_count") + group_ids = fields.One2many('module.overview.group', 'mo_id', string="Groups") + group_count = fields.Integer(string="Group Count", compute="_compute_group_count") + depend_ids = fields.One2many('module.overview.depend', 'mo_id', string="Module Dependacies") + image_ids = fields.One2many('module.overview.image', 'mo_id', string="Images") + store_views_ids = fields.One2many('module.overview.store.view', 'mo_id', string="Store Views") + module_view_count = fields.Integer(string="Module View Count", help="The amount of times the page for this module has been viewed") + module_download_count = fields.Integer(string="Module Download Count", help="The amount of times this module has been downloaded") + module_name = fields.Char(string="Module Name") + version = fields.Char(string="Version Number") + author = fields.Char(string="Author") + summary = fields.Char(string="summary") + icon = fields.Binary(string="Icon") + store_description = fields.Html(string="Store Description") + change_log_raw = fields.Text(string="Change Log") + change_log_html = fields.Html(string="Change Log(html)") + published = fields.Boolean(string="Published", default=True) + + @api.one + @api.depends('menu_ids') + def _compute_menu_count(self): + self.menu_count = len(self.menu_ids) + + @api.one + @api.depends('group_ids') + def _compute_group_count(self): + self.group_count = len(self.group_ids) + + @api.one + @api.depends('models_ids') + def _compute_model_count(self): + self.model_count = len(self.models_ids) + +class ModuleAccess(models.Model): + + _name = "module.access" + _description = "Module Access" + + name = fields.Char(string="Name") + access_token = fields.Char(string="Access Token") + module_access_ids = fields.Many2many('module.overview', tring="Module Access", help="Inactive modules this token allows access too") + +class ModuleOverviewStoreView(models.Model): + + _name = "module.overview.store.view" + _description = "Module Overview Store View" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + ip = fields.Char(string="IP") + ref = fields.Char(string="Ref", help="The URL the person came from") + header = fields.Char(string="Header", help="The raw header which misc info can be extracted from") + +class ModuleOverviewDepend(models.Model): + + _name = "module.overview.depend" + _description = "Module Overview Dependacies" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + name = fields.Char(string="name") + +class ModuleOverviewImage(models.Model): + + _name = "module.overview.image" + _description = "Module Overview Image" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + name = fields.Char(string="File Path") + file_data = fields.Binary(string="File Data") + +class ModuleOverviewWizard(models.Model): + + _name = "module.overview.wizard" + _description = "Module Overview Wizard" + + name = fields.Char(string="Module Name") + +class ModuleOverviewGroup(models.Model): + + _name = "module.overview.group" + _description = "Module Overview Group" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + x_id = fields.Char(string="XML ID") + name = fields.Char(string="Name") + access_ids = fields.One2many('module.overview.group.access', 'mog_id', string="Group Permissions") + +class ModuleOverviewGroupAccess(models.Model): + + _name = "module.overview.group.access" + + mog_id = fields.Many2one('module.overview.group', string="Module Overview", ondelete="cascade") + model_id = fields.Many2one('module.overview.model', string="Model") + perm_read = fields.Boolean(string="Read Permmision") + perm_write = fields.Boolean(string="Write Permmision") + perm_create = fields.Boolean(string="Create Permmision") + perm_unlink = fields.Boolean(string="Delete Permmision") + access_string = fields.Char(string="Access String", compute="_compute_access_string") + + @api.one + @api.depends('perm_read', 'perm_write', 'perm_create', 'perm_unlink') + def _compute_access_string(self): + a_string = "" + + if self.perm_read: + a_string += "Read, " + + if self.perm_write: + a_string += "Write, " + + if self.perm_create: + a_string += "Create, " + + if self.perm_unlink: + a_string += "Delete, " + + self.access_string = a_string[:-2] + +class ModuleOverviewAccess(models.Model): + + _name = "module.overview.access" + _description = "depricted" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + x_id = fields.Char(string="XML ID") + name = fields.Char(string="Name") + model = fields.Char(string="Model") + group = fields.Char(string="Group") + perm_read = fields.Boolean(string="Read Permmision") + perm_write = fields.Boolean(string="Write Permmision") + perm_create = fields.Boolean(string="Create Permmision") + perm_unlink = fields.Boolean(string="Delete Permmision") + access_string = fields.Char(string="Access String", compute="_compute_access_string") + + @api.one + @api.depends('perm_read', 'perm_write', 'perm_create', 'perm_unlink') + def _compute_access_string(self): + a_string = "" + + if self.perm_read: + a_string += "Read, " + + if self.perm_write: + a_string += "Write, " + + if self.perm_create: + a_string += "Create, " + + if self.perm_unlink: + a_string += "Delete, " + + self.access_string = a_string[:-2] + +class ModuleOverviewMenu(models.Model): + + _name = "module.overview.menu" + _description = "Module Overview Menu" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + name = fields.Char(string="Name") + x_id = fields.Char(string="XML ID") + parent = fields.Char(string="Parent Menu") + parent_x_id = fields.Char(string="Parent Menu XML ID") + +class ModuleOverviewModel(models.Model): + + _name = "module.overview.model" + _description = "Module Overview Model" + + mo_id = fields.Many2one('module.overview', string="Module Overview", ondelete="cascade") + name = fields.Char(string="Name") + view_ids = fields.One2many('module.overview.model.view', 'model_id', string="Views") + view_count = fields.Integer(string="View Count", compute="_compute_view_count") + access_ids = fields.One2many('module.overview.model.access', 'model_id', string="Access Rules") + access_count = fields.Integer(string="Access Count", compute="_compute_access_count") + + @api.one + @api.depends('view_ids') + def _compute_view_count(self): + self.view_count = len(self.view_ids) + + @api.one + @api.depends('access_ids') + def _compute_access_count(self): + self.access_count = len(self.access_ids) + +class ModuleOverviewModelAccess(models.Model): + + _name = "module.overview.model.access" + _description = "Module Overview Model Access" + + model_id = fields.Many2one('module.overview.model', string="Model", ondelete="cascade") + name = fields.Char(string="Group Name") + x_id = fields.Char(string="XML ID") + perm_read = fields.Boolean(string="Read Permmision") + perm_write = fields.Boolean(string="Write Permmision") + perm_create = fields.Boolean(string="Create Permmision") + perm_unlink = fields.Boolean(string="Delete Permmision") + access_string = fields.Char(string="Access String", compute="_compute_access_string") + + @api.one + @api.depends('perm_read', 'perm_write', 'perm_create', 'perm_unlink') + def _compute_access_string(self): + a_string = "" + + if self.perm_read: + a_string += "Read, " + + if self.perm_write: + a_string += "Write, " + + if self.perm_create: + a_string += "Create, " + + if self.perm_unlink: + a_string += "Delete, " + + self.access_string = a_string[:-2] + +class ModuleOverviewModelView(models.Model): + + _name = "module.overview.model.view" + _description = "Module Overview Model View" + + model_id = fields.Many2one('module.overview.model', string="Model", ondelete="cascade") + name = fields.Char(string="Name") + x_id = fields.Char(string="XML ID") \ No newline at end of file diff --git a/app_store/security/ir.model.access.csv b/app_store/security/ir.model.access.csv new file mode 100644 index 000000000..b337272b5 --- /dev/null +++ b/app_store/security/ir.model.access.csv @@ -0,0 +1,10 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_module_access","access module.access","model_module_access",,1,1,1,1 +"access_module_overview","access module.overview","model_module_overview",,1,0,0,0 +"access_module_overview_depend","access module.overview.depend","model_module_overview_depend",,1,0,0,0 +"access_module_overview_image","access module.overview.image","model_module_overview_image",,1,0,0,0 +"access_module_overview_group","access module.overview.group","model_module_overview_group",,1,0,0,0 +"access_module_overview_menu","access module.overview.menu","model_module_overview_menu",,1,0,0,0 +"access_module_overview_model","access module.overview.model","model_module_overview_model",,1,0,0,0 +"access_module_overview_model_view","access module.overview.model.view","model_module_overview_model_view",,1,0,0,0 +"access_module_overview_model_access","access module.overview.model.access","model_module_overview_model_access",,1,0,0,0 \ No newline at end of file diff --git a/app_store/static/description/1.jpg b/app_store/static/description/1.jpg new file mode 100644 index 000000000..733df8e0a Binary files /dev/null and b/app_store/static/description/1.jpg differ diff --git a/app_store/static/description/2.jpg b/app_store/static/description/2.jpg new file mode 100644 index 000000000..a51035890 Binary files /dev/null and b/app_store/static/description/2.jpg differ diff --git a/app_store/static/description/index.html b/app_store/static/description/index.html new file mode 100644 index 000000000..97259110c --- /dev/null +++ b/app_store/static/description/index.html @@ -0,0 +1,10 @@ +
+

Description

+Your own app store
+
+

Instructions

+1. Create a folder named "apps" in the Odoo instances home directory e.g. "/odoo/apps"
+2. Upload your module folders to the newly created 'apps' folder
+3. Go to Settings->App Store->Update App Store and click 'Update Module List'
+4. View your modules on the App Store menu on your website
+
\ No newline at end of file diff --git a/app_store/views/appstore_account_views.xml b/app_store/views/appstore_account_views.xml new file mode 100644 index 000000000..a5f443df8 --- /dev/null +++ b/app_store/views/appstore_account_views.xml @@ -0,0 +1,42 @@ + + + + + appstore.account.view.form + appstore.account + +
+
+
+ + + + + + + + + +
+
+
+ + + appstore.account.view.tree + appstore.account + + + + + + + + + App Store Accounts + appstore.account + form + tree,form + + +
\ No newline at end of file diff --git a/app_store/views/menus.xml b/app_store/views/menus.xml new file mode 100644 index 000000000..d99213404 --- /dev/null +++ b/app_store/views/menus.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app_store/views/module_access_views.xml b/app_store/views/module_access_views.xml new file mode 100644 index 000000000..a1c5319f2 --- /dev/null +++ b/app_store/views/module_access_views.xml @@ -0,0 +1,37 @@ + + + + + module_access.view.form + module.access + +
+ + + + + +
+
+
+ + + module_access.view.tree + module.access + + + + + + + + + + + Module Access + module.access + form + tree,form + + +
\ No newline at end of file diff --git a/app_store/views/module_overview_templates.xml b/app_store/views/module_overview_templates.xml new file mode 100644 index 000000000..f16f5eed5 --- /dev/null +++ b/app_store/views/module_overview_templates.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app_store/views/module_overview_views.xml b/app_store/views/module_overview_views.xml new file mode 100644 index 000000000..820cb3dcf --- /dev/null +++ b/app_store/views/module_overview_views.xml @@ -0,0 +1,108 @@ + + + + + module.overview.view.form + module.overview + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + module.overview.model.view.form + module.overview.model + +
+ + + + + +
+
+
+ + + module.overview.view.tree + module.overview + + + + + + + + + Custom Modules + module.overview + form + tree,form + + + + module.overview.wizard form view + module.overview.wizard + +
+
+ + + + +
+
+ + + module.custom.updates tree view + module.custom.updates + + + + + + + + + + + + Check for Custom Apps Updates + module.custom.updates + form + form + new + + + + +
\ No newline at end of file diff --git a/crm_custom_fields/__init__.py b/crm_custom_fields/__init__.py new file mode 100644 index 000000000..5305644df --- /dev/null +++ b/crm_custom_fields/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/crm_custom_fields/__manifest__.py b/crm_custom_fields/__manifest__.py new file mode 100644 index 000000000..47af7c8e5 --- /dev/null +++ b/crm_custom_fields/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': "CRM Custom Fields", + 'version': "1.4.0", + 'author': "Sythil Tech", + 'category': "CRM", + 'summary': "Allows users in the 'Sales / Manager' group to add additional fields to the partner form", + 'license':'LGPL-3', + 'data': [ + 'views/res_partner_views.xml', + 'views/ir_model_views.xml', + 'views/sale_config_settings_convert_views.xml', + 'security/ir.model.access.csv', + 'data/crm.custom.fields.widget.csv', + ], + 'demo': [], + 'depends': ['crm'], + 'images':[ + 'static/description/1.jpg', + 'static/description/2.jpg', + 'static/description/3.jpg', + ], + 'installable': True, +} \ No newline at end of file diff --git a/crm_custom_fields/data/crm.custom.fields.widget.csv b/crm_custom_fields/data/crm.custom.fields.widget.csv new file mode 100644 index 000000000..cdf8e4548 --- /dev/null +++ b/crm_custom_fields/data/crm.custom.fields.widget.csv @@ -0,0 +1,2 @@ +"id","name","internal_name" +"url","URL","url" \ No newline at end of file diff --git a/crm_custom_fields/doc/changelog.rst b/crm_custom_fields/doc/changelog.rst new file mode 100644 index 000000000..f428dba03 --- /dev/null +++ b/crm_custom_fields/doc/changelog.rst @@ -0,0 +1,3 @@ +v1.0 +==== +* Port to version 11 \ No newline at end of file diff --git a/crm_custom_fields/models/__init__.py b/crm_custom_fields/models/__init__.py new file mode 100644 index 000000000..2850ffb13 --- /dev/null +++ b/crm_custom_fields/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import ir_model +from . import ir_model_fields +from . import res_partner +from . import crm_lead +from . import sale_config_settings \ No newline at end of file diff --git a/crm_custom_fields/models/crm_lead.py b/crm_custom_fields/models/crm_lead.py new file mode 100644 index 000000000..42555108d --- /dev/null +++ b/crm_custom_fields/models/crm_lead.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools, SUPERUSER_ID + +class CRMLeadConert(models.Model): + + _inherit = "crm.lead" + + @api.multi + def _lead_create_contact(self, name, is_company, parent_id=False): + """ extract data from lead to create a partner + :param name : furtur name of the partner + :param is_company : True if the partner is a company + :param parent_id : id of the parent partner (False if no parent) + :returns res.partner record + """ + email_split = tools.email_split(self.email_from) + values = { + 'name': name, + 'user_id': self.user_id.id, + 'comment': self.description, + 'team_id': self.team_id.id, + 'parent_id': parent_id, + 'phone': self.phone, + 'mobile': self.mobile, + 'email': email_split[0] if email_split else False, + 'fax': self.fax, + 'title': self.title.id, + 'function': self.function, + 'street': self.street, + 'street2': self.street2, + 'zip': self.zip, + 'city': self.city, + 'country_id': self.country_id.id, + 'state_id': self.state_id.id, + 'is_company': is_company, + 'type': 'contact' + } + + for convert_field in self.env['sale.config.settings.convert'].search([]): + values[convert_field.partner_field_id.name] = self[convert_field.lead_field_id.name] + + return self.env['res.partner'].create(values) + diff --git a/crm_custom_fields/models/ir_model.py b/crm_custom_fields/models/ir_model.py new file mode 100644 index 000000000..2bbf724b1 --- /dev/null +++ b/crm_custom_fields/models/ir_model.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + +class IrModelCRMFields(models.Model): + + _inherit = "ir.model" + + + custom_field_ids = fields.One2many('ir.model.fields', 'custom_model_id', string="Custom Fields", domain=[('crm_custom_field','=',True)]) + + @api.one + def fake_save(self): + """Function does nothing, save gets called after""" + pass + + @api.multi + def write(self, values): + + ins = super(IrModelCRMFields, self).write(values) + + partner_custom_form_fields = self.env['ir.model.data'].sudo().get_object('crm_custom_fields', 'view_partner_form_inherit_crm_custom_fields') + + custom_form_fields_string = "" + + custom_form_fields_string += "\n" + custom_form_fields_string += " \n" + custom_form_fields_string += " \n" + + for custom_field in self.env['ir.model.fields'].sudo().search([('crm_custom_field','=',True)]): + custom_form_fields_string += " \n" + custom_form_fields_string += " \n" + custom_form_fields_string += " \n" + custom_form_fields_string += "" + + partner_custom_form_fields.arch = custom_form_fields_string + + return ins \ No newline at end of file diff --git a/crm_custom_fields/models/ir_model_fields.py b/crm_custom_fields/models/ir_model_fields.py new file mode 100644 index 000000000..039164575 --- /dev/null +++ b/crm_custom_fields/models/ir_model_fields.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +import logging +_logger = logging.getLogger(__name__) +from openerp import api, fields, models + +class IrModelFieldsCRMFields(models.Model): + + _inherit = "ir.model.fields" + + custom_model_id = fields.Many2one('ir.model', string="Custom Model") + crm_limited_types = fields.Selection([('char','Single Line Textbox'), ('text','Multi Line Textbox'), ('date','Date'), ('datetime','Date Time'), ('selection', 'Static Dropdown')], default="char", string="Field Type") + crm_custom_name = fields.Char(string="Field Name") + crm_custom_field = fields.Boolean(string="Custom CRM Field") + crm_custom_field_widget = fields.Many2one('crm.custom.fields.widget', string="Widget") + crm_custom_field_selection_ids = fields.One2many('ir.model.fields.selections', 'imf_id', string="Selection Options") + + @api.onchange('crm_limited_types') + def _onchange_crm_limited_types(self): + self.ttype = self.crm_limited_types + + @api.onchange('crm_custom_field_selection_ids') + def _onchange_crm_custom_field_selection_ids(self): + sel_string = "" + for sel in self.crm_custom_field_selection_ids: + sel_string += "('" + sel.internal_name + "','" + sel.name + "'), " + + self.selection = "[" + sel_string[:-2] + "]" + + @api.model + def create(self, values): + """Assign name when it's actually saved overwise they all get the same ID""" + + if 'crm_custom_field' in values: + temp_name = "x_custom_" + values['field_description'] + temp_name = temp_name.replace(" ","_").lower() + values['name'] = temp_name + values['model_id'] = self.env['ir.model.data'].get_object('base','model_res_partner').id + _logger.error(temp_name) + + return super(IrModelFieldsCRMFields, self).create(values) + +class CrmCustomFieldsWidget(models.Model): + + _name = "crm.custom.fields.widget" + + name = fields.Char(string="Name") + internal_name = fields.Char(string="Internal Name", help="The technicial name of the widget") + +class IrModelFieldsCRMFieldsSelection(models.Model): + + _name = "ir.model.fields.selections" + + imf_id = fields.Many2one('ir.model.fields', string="Field") + name = fields.Char(string="Name", required="True") + internal_name = fields.Char(string="Internal Name", required="True") \ No newline at end of file diff --git a/crm_custom_fields/models/res_partner.py b/crm_custom_fields/models/res_partner.py new file mode 100644 index 000000000..153ff7273 --- /dev/null +++ b/crm_custom_fields/models/res_partner.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + +class ResPartnerCustomFields(models.Model): + + _inherit = "res.partner" + + @api.multi + def open_custom_field_form(self): + my_model = self.env['ir.model'].search([('model','=','res.partner')])[0] + custom_model_view = self.env['ir.model.data'].sudo().get_object('crm_custom_fields','ir_model_view_form_custom_crm_fields') + + return { + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'ir.model', + 'res_id': my_model.id, + 'view_id': custom_model_view.id, + 'target': 'new', + } \ No newline at end of file diff --git a/crm_custom_fields/models/sale_config_settings.py b/crm_custom_fields/models/sale_config_settings.py new file mode 100644 index 000000000..3e471b7f6 --- /dev/null +++ b/crm_custom_fields/models/sale_config_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + +class SaleConfigSettingsConvert(models.Model): + + _name = "sale.config.settings.convert" + + lead_field_id = fields.Many2one('ir.model.fields', string="Lead Field", domain="[('model','=', 'crm.lead')]") + partner_field_id = fields.Many2one('ir.model.fields', string="Partner Field", domain="[('model','=', 'res.partner')]") \ No newline at end of file diff --git a/crm_custom_fields/security/ir.model.access.csv b/crm_custom_fields/security/ir.model.access.csv new file mode 100644 index 000000000..6d7523587 --- /dev/null +++ b/crm_custom_fields/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ir_model_fields,access ir.model.fields,model_ir_model_fields,sales_team.group_sale_manager,1,1,1,1 +access_ir_model,access ir.model,model_ir_model,sales_team.group_sale_manager,1,1,0,0 \ No newline at end of file diff --git a/crm_custom_fields/static/description/1.jpg b/crm_custom_fields/static/description/1.jpg new file mode 100644 index 000000000..f9720c444 Binary files /dev/null and b/crm_custom_fields/static/description/1.jpg differ diff --git a/crm_custom_fields/static/description/2.jpg b/crm_custom_fields/static/description/2.jpg new file mode 100644 index 000000000..1e3f544ae Binary files /dev/null and b/crm_custom_fields/static/description/2.jpg differ diff --git a/crm_custom_fields/static/description/3.jpg b/crm_custom_fields/static/description/3.jpg new file mode 100644 index 000000000..5cb5fdfff Binary files /dev/null and b/crm_custom_fields/static/description/3.jpg differ diff --git a/crm_custom_fields/static/description/icon.png b/crm_custom_fields/static/description/icon.png new file mode 100644 index 000000000..0249aa1b1 Binary files /dev/null and b/crm_custom_fields/static/description/icon.png differ diff --git a/crm_custom_fields/static/description/index.html b/crm_custom_fields/static/description/index.html new file mode 100644 index 000000000..78d16ed25 --- /dev/null +++ b/crm_custom_fields/static/description/index.html @@ -0,0 +1,48 @@ +
+

Description

+Allows users in the 'Sales / Manager' group to add additional fields to the partner form
+
+
+
+

Add new fields without having to add python or edit xml.

+
+
+ Add CRM Fields +
+
+
+

+Designed to simplify the adding of new fields to the CRM.
+
+Instructions
+1. Go to any customer record and open the 'Custom Fields' tab
+2. Click on the 'Add Custom Field' button
+3. Refresh your browser to see the new fields under the Custom Fields tab
+

+
+
+
+ +
+
+

Simple field configuration form

+
+

+Intuitive add field form
+
+Instructions
+1. Go to any customer record and open the 'Custom Fields' tab
+2. Click on the 'Add Custom Field' button
+3. Click 'Add an item'
+

+
+
+
+ +Simple add field interface + +
+
+
+
+
\ No newline at end of file diff --git a/crm_custom_fields/views/ir_model_views.xml b/crm_custom_fields/views/ir_model_views.xml new file mode 100644 index 000000000..4b7446433 --- /dev/null +++ b/crm_custom_fields/views/ir_model_views.xml @@ -0,0 +1,59 @@ + + + + + + Custom CRM fields Form View + ir.model + 200 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/crm_custom_fields/views/res_partner_views.xml b/crm_custom_fields/views/res_partner_views.xml new file mode 100644 index 000000000..09a670d03 --- /dev/null +++ b/crm_custom_fields/views/res_partner_views.xml @@ -0,0 +1,23 @@ + + + + + + Custom CRM fields Form View + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website_support/views/website_support_ticket_views.xml b/website_support/views/website_support_ticket_views.xml new file mode 100644 index 000000000..7bb3f136e --- /dev/null +++ b/website_support/views/website_support_ticket_views.xml @@ -0,0 +1,194 @@ + + + + + + website.support.ticket.form.view + website.support.ticket + +
+
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+
+ + + website.support.ticket.kanban.view + website.support.ticket + + + + + + + + + +
+
+
+ +
    +
  • +
  • +
  • +
  • +
+ +
+ + + + + + + + website.support.ticket.form.search + website.support.ticket + + + + + + + + + + + + + + + website.support.ticket.form.graph + website.support.ticket + + + + + + + + + website.support.ticket tree view + website.support.ticket + + + + + + + + + + + + + + + + + + + + Support Tickets + website.support.ticket + tree,kanban,form,graph + +

+ No Support Tickets found +

+
+
+ + + Support Tickets + website.support.ticket + tree,kanban,form,graph + {"search_default_unattended_tickets":1, 'auto_refresh': 1, 'default_create_user_id': uid} + +

+ No Support Tickets found +

+
+
+ + + \ No newline at end of file diff --git a/website_support/views/website_support_ticket_views_new.xml b/website_support/views/website_support_ticket_views_new.xml new file mode 100644 index 000000000..9415ea495 --- /dev/null +++ b/website_support/views/website_support_ticket_views_new.xml @@ -0,0 +1,220 @@ + + + + + + website.support.ticket.form.view + website.support.ticket + +
+
+ +
+ + +
+
+
+ + +
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + website.support.ticket.kanban.view + website.support.ticket + + + + + + + + + +
+
+
+ +
    +
  • +
  • +
  • +
  • +
+ +
+ + + + + + + + website.support.ticket.form.search + website.support.ticket + + + + + + + + + + + + + + + website.support.ticket.form.graph + website.support.ticket + + + + + + + + + website.support.ticket tree view + website.support.ticket + + + + + + + + + + + + + + + + + + + + Support Tickets + website.support.ticket + tree,kanban,form,graph + +

+ No Support Tickets found +

+
+
+ + + Support Tickets + website.support.ticket + tree,kanban,form,graph + {"search_default_unattended_tickets":1, 'auto_refresh': 1, 'default_create_user_id': uid} + +

+ No Support Tickets found +

+
+
+ + + \ No newline at end of file diff --git a/website_support_analytic_timesheets/__init__.py b/website_support_analytic_timesheets/__init__.py new file mode 100644 index 000000000..5305644df --- /dev/null +++ b/website_support_analytic_timesheets/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/website_support_analytic_timesheets/__manifest__.py b/website_support_analytic_timesheets/__manifest__.py new file mode 100644 index 000000000..ec3d8ae18 --- /dev/null +++ b/website_support_analytic_timesheets/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': "Website Help Desk / Support Ticket - Analytic Timesheets", + 'version': "1.0.7", + 'author': "Sythil Tech", + 'category': "Tools", + 'summary':'Track time spend on tickets', + 'license':'LGPL-3', + 'data': [ + 'views/website_support_ticket_views.xml', + 'views/website_support_ticket_templates.xml', + 'views/account_analytic_line_views.xml', + 'views/website_support_settings_views.xml', + 'security/ir.rule.csv', + 'security/ir.model.access.csv', + 'data/account.analytic.account.csv' + ], + 'demo': [], + 'depends': ['website_support','hr_timesheet'], + 'images':[ + 'static/description/1.jpg', + ], + 'installable': True, +} \ No newline at end of file diff --git a/website_support_analytic_timesheets/data/account.analytic.account.csv b/website_support_analytic_timesheets/data/account.analytic.account.csv new file mode 100644 index 000000000..4799f45c2 --- /dev/null +++ b/website_support_analytic_timesheets/data/account.analytic.account.csv @@ -0,0 +1,2 @@ +"id","name" +"customer_support_account","Customer Support" \ No newline at end of file diff --git a/website_support_analytic_timesheets/doc/changelog.rst b/website_support_analytic_timesheets/doc/changelog.rst new file mode 100644 index 000000000..c5935b680 --- /dev/null +++ b/website_support_analytic_timesheets/doc/changelog.rst @@ -0,0 +1,32 @@ +v1.0.7 +====== +* Move timesheet into tab and fix permission issue + +v1.0.6 +====== +* Compatablility fix for version 1.3.1 + +v1.0.5 +====== +* Report that breaks down activities by day and tech + +v1.0.4 +====== +* Field employee_id is now automatically filled in from the support ticket side + +v1.0.3 +====== +* Fix access rule preventing adding timesheet line without project + +v1.0.2 +====== +* Fix email Jinja for loop getting messed up inside table + +v1.0.1 +====== +* Filter out closed email template from compose window +* Remove timesheet table if timesheets is empty (if people bypass the close button...) + +v1.0 +==== +* Port to version 11 \ No newline at end of file diff --git a/website_support_analytic_timesheets/models/__init__.py b/website_support_analytic_timesheets/models/__init__.py new file mode 100644 index 000000000..1ebb7ec64 --- /dev/null +++ b/website_support_analytic_timesheets/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import account_analytic_line +from . import website_support_ticket +from . import website_support_settings +from . import reports \ No newline at end of file diff --git a/website_support_analytic_timesheets/models/account_analytic_line.py b/website_support_analytic_timesheets/models/account_analytic_line.py new file mode 100644 index 000000000..67facefce --- /dev/null +++ b/website_support_analytic_timesheets/models/account_analytic_line.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models + +class WebsiteSupportTicketInheritAccountAnalyticLine(models.Model): + + _inherit = "account.analytic.line" + + support_ticket_id = fields.Many2one('website.support.ticket', string="Support Ticket") + person_name = fields.Char(related="support_ticket_id.person_name", string="Customer Name") + ticket_number_display = fields.Char(related="support_ticket_id.ticket_number", string="Ticket Number") + state = fields.Many2one('website.support.ticket.states', readonly=True, related="support_ticket_id.state", string="State") + open_time = fields.Datetime(related="support_ticket_id.create_date", string="Open Time") + close_time = fields.Datetime(related="support_ticket_id.close_time", string="Close Time") + planned_hours = fields.Float(string='Initially Planned Hours', related="task_id.planned_hours", help='Estimated time to do the task, usually set by the project manager when the task is in draft state.') + remaining_hours = fields.Float(string='Remaining Hours', related="task_id.remaining_hours", digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task.") + total_hours = fields.Float(string='Total', related="task_id.total_hours", help="Computed as: Time Spent + Remaining Time.") + effective_hours = fields.Float(string='Hours Spent', related="task_id.effective_hours", help="Computed using the sum of the task work done.") \ No newline at end of file diff --git a/website_support_analytic_timesheets/models/reports.py b/website_support_analytic_timesheets/models/reports.py new file mode 100644 index 000000000..e84affc0f --- /dev/null +++ b/website_support_analytic_timesheets/models/reports.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +import time +from openerp import api, models, _ + +import logging +_logger = logging.getLogger(__name__) + +class ReportWebsetAupportAnalyticTimesheetsSupportTechReport(models.Model): + + _name = "report.website_support_analytic_timesheets.strt" + + @api.multi + def get_report_values(self, docids, data=None): + docs = self.env['account.analytic.line'].browse(docids) + + + date_dict = {} + for timesheet_line in docs: + #Group by Date + if str(timesheet_line.date) not in date_dict: + date_dict[str(timesheet_line.date)] = {} + + #Sub group by employee name + if str(timesheet_line.employee_id.name) not in date_dict[str(timesheet_line.date)]: + date_dict[str(timesheet_line.date)][str(timesheet_line.employee_id.name)] = [] + + date_dict[str(timesheet_line.date)][str(timesheet_line.employee_id.name)].append(timesheet_line) + + _logger.error(date_dict) + + return { + 'doc_model': 'account.analytic.line', + 'docs': date_dict + } \ No newline at end of file diff --git a/website_support_analytic_timesheets/models/website_support_settings.py b/website_support_analytic_timesheets/models/website_support_settings.py new file mode 100644 index 000000000..11884273a --- /dev/null +++ b/website_support_analytic_timesheets/models/website_support_settings.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import logging +_logger = logging.getLogger(__name__) +import requests +from openerp.http import request +import odoo + +from openerp import api, fields, models + +class WebsiteSupportSettingsInherit(models.TransientModel): + + _inherit = 'website.support.settings' + + timesheet_default_project_id = fields.Many2one('project.project', string="Default Timesheet Project") + + @api.multi + def set_values(self): + super(WebsiteSupportSettingsInherit, self).set_values() + self.env['ir.default'].set('website.support.settings', 'timesheet_default_project_id', self.timesheet_default_project_id.id) + + @api.model + def get_values(self): + res = super(WebsiteSupportSettingsInherit, self).get_values() + res.update( + timesheet_default_project_id=self.env['ir.default'].get('website.support.settings', 'timesheet_default_project_id'), + ) + return res \ No newline at end of file diff --git a/website_support_analytic_timesheets/models/website_support_ticket.py b/website_support_analytic_timesheets/models/website_support_ticket.py new file mode 100644 index 000000000..88101f4a3 --- /dev/null +++ b/website_support_analytic_timesheets/models/website_support_ticket.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from openerp import api, fields, models +import logging +_logger = logging.getLogger(__name__) + +from odoo.exceptions import UserError + +class WebsiteSupportTicketInheritTimesheets(models.Model): + + _inherit = "website.support.ticket" + + analytic_timesheet_ids = fields.One2many('account.analytic.line', 'support_ticket_id', string="Timesheet") + timesheet_project_id = fields.Many2one('project.project', string="Timesheet Project", compute="_compute_timesheet_project_id") + analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", compute="_compute_analytic_account_id") + + @api.multi + def open_close_ticket_wizard(self): + + timesheet_count = len(self.analytic_timesheet_ids) + + if timesheet_count == 0: + raise UserError("Timesheets must be filled in before the ticket can be closed") + + return { + 'name': "Close Support Ticket", + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'website.support.ticket.close', + 'context': {'default_ticket_id': self.id}, + 'target': 'new' + } + + @api.multi + def _compute_analytic_account_id(self): + + default_analytic_account = self.env['ir.model.data'].get_object('website_support_analytic_timesheets', 'customer_support_account') + + for record in self: + record.analytic_account_id = default_analytic_account.id + + @api.multi + def _compute_timesheet_project_id(self): + + setting_timesheet_default_project_id = self.env['ir.default'].get('website.support.settings', 'timesheet_default_project_id') + + for record in self: + + if setting_timesheet_default_project_id: + record.timesheet_project_id = setting_timesheet_default_project_id diff --git a/website_support_analytic_timesheets/security/ir.model.access.csv b/website_support_analytic_timesheets/security/ir.model.access.csv new file mode 100644 index 000000000..25a919772 --- /dev/null +++ b/website_support_analytic_timesheets/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_analytic_line,access account.analytic.line,model_account_analytic_line,website_support.support_staff,1,1,1,1 \ No newline at end of file diff --git a/website_support_analytic_timesheets/security/ir.rule.csv b/website_support_analytic_timesheets/security/ir.rule.csv new file mode 100644 index 000000000..5a8aa141d --- /dev/null +++ b/website_support_analytic_timesheets/security/ir.rule.csv @@ -0,0 +1,2 @@ +id,domain_force +"hr_timesheet.timesheet_line_rule_user","[('user_id', '=', user.id)]" \ No newline at end of file diff --git a/website_support_analytic_timesheets/static/description/1.jpg b/website_support_analytic_timesheets/static/description/1.jpg new file mode 100644 index 000000000..56f1aef14 Binary files /dev/null and b/website_support_analytic_timesheets/static/description/1.jpg differ diff --git a/website_support_analytic_timesheets/static/description/2.jpg b/website_support_analytic_timesheets/static/description/2.jpg new file mode 100644 index 000000000..316dec99c Binary files /dev/null and b/website_support_analytic_timesheets/static/description/2.jpg differ diff --git a/website_support_analytic_timesheets/static/description/icon.png b/website_support_analytic_timesheets/static/description/icon.png new file mode 100644 index 000000000..eee8c3877 Binary files /dev/null and b/website_support_analytic_timesheets/static/description/icon.png differ diff --git a/website_support_analytic_timesheets/static/description/index.html b/website_support_analytic_timesheets/static/description/index.html new file mode 100644 index 000000000..7f6460f01 --- /dev/null +++ b/website_support_analytic_timesheets/static/description/index.html @@ -0,0 +1,10 @@ +
+

Description

+

Track time spend on tickets

+Have a record of the amount of time that gets spent on support tickets +Support Ticket Timesheet +

Tech Report

+Display a report that breaks down each techs activities (Task / Support Ticket) by day
+CRM->Timesheet Reports->Print->Support Tech Report
+Support Ticket Timesheet Reports +
\ No newline at end of file diff --git a/website_support_analytic_timesheets/views/account_analytic_line_views.xml b/website_support_analytic_timesheets/views/account_analytic_line_views.xml new file mode 100644 index 000000000..e17492e23 --- /dev/null +++ b/website_support_analytic_timesheets/views/account_analytic_line_views.xml @@ -0,0 +1,42 @@ + + + + + + account.analytic.line inherit support ticket + account.analytic.line + + + + + + + + + + + + + + + + + + + + + + + + + + Support Ticket Timesheet Report + account.analytic.line + tree + + + + + + + \ No newline at end of file diff --git a/website_support_analytic_timesheets/views/website_support_settings_views.xml b/website_support_analytic_timesheets/views/website_support_settings_views.xml new file mode 100644 index 000000000..882600bdc --- /dev/null +++ b/website_support_analytic_timesheets/views/website_support_settings_views.xml @@ -0,0 +1,17 @@ + + + + + + website.support.settings timesheet inherit + website.support.settings + + + + + + + + + + \ No newline at end of file diff --git a/website_support_analytic_timesheets/views/website_support_ticket_templates.xml b/website_support_analytic_timesheets/views/website_support_ticket_templates.xml new file mode 100644 index 000000000..d1f07a512 --- /dev/null +++ b/website_support_analytic_timesheets/views/website_support_ticket_templates.xml @@ -0,0 +1,99 @@ + + + + + + Support Ticket Closed Analytic Timesheets + + ${user.email|safe} + ${object.email|safe} + Your support ticket has been closed + + + + Dear ${object.person_name},

+

Your support ticket has been closed by our staff, here is the final comment

+

${object.close_comment or ''}

+% if object.analytic_timesheet_ids: +

Time Sheet

+
+
+
+
Staff Member
+
Time
+
+
+
+ % for timeslot in object.analytic_timesheet_ids: +
+
${timeslot.user_id.name}
+
${timeslot.unit_amount}
+
+ % endfor +
+
+% endif +
+Ticket Number: ${object.ticket_number or object.id}
+Ticket Category: ${object.category.name or ''} +
+Ticket Description:
+${object.description|safe} + ]]> +
+
+ + + + + +
+
\ No newline at end of file diff --git a/website_support_analytic_timesheets/views/website_support_ticket_views.xml b/website_support_analytic_timesheets/views/website_support_ticket_views.xml new file mode 100644 index 000000000..015443423 --- /dev/null +++ b/website_support_analytic_timesheets/views/website_support_ticket_views.xml @@ -0,0 +1,32 @@ + + + + + + website.support.ticket.view.form.inherit.timesheets + website.support.ticket + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website_support_billing/__init__.py b/website_support_billing/__init__.py new file mode 100644 index 000000000..5305644df --- /dev/null +++ b/website_support_billing/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/website_support_billing/__manifest__.py b/website_support_billing/__manifest__.py new file mode 100644 index 000000000..4dfc447a1 --- /dev/null +++ b/website_support_billing/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': "Website Help Desk / Support Ticket - Billing", + 'version': "1.0.0", + 'author': "Sythil Tech", + 'category': "Tools", + 'summary':'Generate invoices based on amount of time spent on ticket / tasks', + 'license':'LGPL-3', + 'data': [ + 'views/res_partner_views.xml', + 'security/ir.model.access.csv', + ], + 'demo': [], + 'depends': ['website_support','website_support_analytic_timesheets', 'account_invoicing'], + 'images':[ + 'static/description/1.jpg', + ], + 'installable': True, +} \ No newline at end of file diff --git a/website_support_billing/doc/changelog.rst b/website_support_billing/doc/changelog.rst new file mode 100644 index 000000000..f160755b1 --- /dev/null +++ b/website_support_billing/doc/changelog.rst @@ -0,0 +1,3 @@ +v1.0.0 +====== +* Initial Release \ No newline at end of file diff --git a/website_support_billing/models/__init__.py b/website_support_billing/models/__init__.py new file mode 100644 index 000000000..cd5f4d832 --- /dev/null +++ b/website_support_billing/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import res_partner \ No newline at end of file diff --git a/website_support_billing/models/res_partner.py b/website_support_billing/models/res_partner.py new file mode 100644 index 000000000..ff999e6ba --- /dev/null +++ b/website_support_billing/models/res_partner.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import logging +_logger = logging.getLogger(__name__) +import datetime + +from openerp import api, fields, models, tools + +class ResPartnerSupportBilling(models.Model): + + _inherit = "res.partner" + + @api.multi + def support_billing_action(self): + self.ensure_one() + + return { + 'name': 'Support Billing Invoice Wizard', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'res.partner.support.billing.wizard', + 'type': 'ir.actions.act_window', + 'target': 'new', + 'context': {'default_partner_id': self.id} + } + +class ResPartnerSupportBillingWizard(models.Model): + + _name = "res.partner.support.billing.wizard" + + partner_id = fields.Many2one('res.partner', string="Partner") + start_date = fields.Date(string="Start Date", required="True") + end_date = fields.Date(string="End Date", required="True") + per_hour_charge = fields.Float(string="Per Hour Charge") + + @api.multi + def generate_invoice(self): + self.ensure_one() + + new_invoice = self.env['account.invoice'].create({ + 'partner_id': self.partner_id.id, + 'account_id': self.partner_id.property_account_receivable_id.id, + 'fiscal_position_id': self.partner_id.property_account_position_id.id, + }) + + if self.partner_id.company_type == "person": + + #Get all the invoice lines for just a single individual + self.partner_ticket_task_charge(new_invoice, self.partner_id) + + elif self.partner_id.company_type == "company": + + #Loop through all contacts and get the invoice lines + for company_contact in self.partner_id.child_ids: + self.partner_ticket_task_charge(new_invoice, company_contact) + + new_invoice.compute_taxes() + + return { + 'name': 'Support Billing Invoice', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'account.invoice', + 'type': 'ir.actions.act_window', + 'res_id': new_invoice.id + } + + def partner_ticket_task_charge(self, new_invoice, partner): + + partner_tasks = self.env['project.task'].search([('partner_id','=', partner.id), ('create_date','>=',self.start_date), ('create_date','<=',self.end_date)]) + partner_tickets = self.env['website.support.ticket'].search([('partner_id','=', partner.id), ('create_date','>=',self.start_date), ('create_date','<=',self.end_date)]) + + for support_ticket in partner_tickets: + total_hours = 0 + + for timesheet_line in support_ticket.analytic_timesheet_ids: + total_hours += timesheet_line.unit_amount + + total_charge = total_hours * self.per_hour_charge + + line_values = { + 'name': support_ticket.subject, + 'price_unit': total_charge, + 'invoice_id': new_invoice.id, + 'account_id': new_invoice.journal_id.default_credit_account_id.id + } + + new_invoice.write({'invoice_line_ids': [(0, 0, line_values)]}) + + for task in partner_tasks: + total_hours = 0 + + for timesheet_line in task.timesheet_ids: + total_hours += timesheet_line.unit_amount + + total_charge = total_hours * self.per_hour_charge + + line_values = { + 'name': task.name, + 'price_unit': total_charge, + 'invoice_id': new_invoice.id, + 'account_id': new_invoice.journal_id.default_credit_account_id.id + } + + new_invoice.write({'invoice_line_ids': [(0, 0, line_values)]}) \ No newline at end of file diff --git a/website_support_billing/security/ir.model.access.csv b/website_support_billing/security/ir.model.access.csv new file mode 100644 index 000000000..362b55cbd --- /dev/null +++ b/website_support_billing/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_res_partner_support_billing_wizard,access res.partner.support.billing.wizard,model_res_partner_support_billing_wizard,,1,1,1,0 \ No newline at end of file diff --git a/website_support_billing/static/description/1.jpg b/website_support_billing/static/description/1.jpg new file mode 100644 index 000000000..9fb80ee1c Binary files /dev/null and b/website_support_billing/static/description/1.jpg differ diff --git a/website_support_billing/static/description/icon.png b/website_support_billing/static/description/icon.png new file mode 100644 index 000000000..eee8c3877 Binary files /dev/null and b/website_support_billing/static/description/icon.png differ diff --git a/website_support_billing/static/description/index.html b/website_support_billing/static/description/index.html new file mode 100644 index 000000000..135ac2887 --- /dev/null +++ b/website_support_billing/static/description/index.html @@ -0,0 +1,6 @@ +
+

Description

+

Send invoices

+Generate invoices based on amount of time spent on ticket / tasks
+Support Ticket Invoices +
\ No newline at end of file diff --git a/website_support_billing/views/res_partner_views.xml b/website_support_billing/views/res_partner_views.xml new file mode 100644 index 000000000..3392d7160 --- /dev/null +++ b/website_support_billing/views/res_partner_views.xml @@ -0,0 +1,31 @@ + + + + + + res.partner.support.billing.wizard.form.view + res.partner.support.billing.wizard + +
+ + + + + + +