Pay with
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 417fb1950d1..a0d348849eb 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -70,6 +70,13 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _timesheet_compute_theorical_revenue(self):
+ for timesheet in self:
+ values = timesheet._timesheet_compute_theorical_revenue_values()
+ timesheet.write(values)
+ return True
+
+ @api.multi
+ def _timesheet_compute_theorical_revenue_values(self):
""" This method set the theorical revenue on the current timesheet lines.
If invoice on delivered quantity:
@@ -82,53 +89,55 @@ class AccountAnalyticLine(models.Model):
else:
0
"""
- for timesheet in self:
- # find the timesheet UoM
- timesheet_uom = timesheet.product_uom_id
- if not timesheet_uom: # fallback on default company timesheet UoM
- timesheet_uom = self.env.user.company_id.project_time_mode_id
+ self.ensure_one()
+ timesheet = self
- # default values
- unit_amount = timesheet.unit_amount
- so_line = timesheet.so_line
- values = {
- 'timesheet_revenue': 0.0,
- 'timesheet_invoice_type': 'non_billable_project' if not timesheet.task_id else 'non_billable',
- }
- # set the revenue and billable type according to the product and the SO line
- if timesheet.task_id and so_line.product_id.type == 'service':
- # find the analytic account to convert revenue into its currency
- analytic_account = timesheet.account_id
- # convert the unit of mesure into hours
- sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
- sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
+ # find the timesheet UoM
+ timesheet_uom = timesheet.product_uom_id
+ if not timesheet_uom: # fallback on default company timesheet UoM
+ timesheet_uom = self.env.user.company_id.project_time_mode_id
- # calculate the revenue on the timesheet
- if so_line.product_id.invoice_policy == 'delivery':
- values['timesheet_revenue'] = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
- values['timesheet_invoice_type'] = 'billable_time' if so_line.product_id.service_type == 'timesheet' else 'billable_fixed'
- elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type == 'timesheet':
- quantity_hour = unit_amount
- if so_line.product_uom.category_id == timesheet_uom.category_id:
- quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
- # compute the total revenue the SO since we are in fixed price
- total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
- # compute the total revenue already existing (without the current timesheet line)
- domain = [('so_line', '=', so_line.id)]
- if timesheet.ids:
- domain += [('id', 'not in', timesheet.ids)]
- analytic_lines = timesheet.search(domain)
- total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue'))
- # compute (new) revenue of current timesheet line
- values['timesheet_revenue'] = min(
- analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)),
- total_revenue_so - total_revenue_invoiced
- )
- values['timesheet_invoice_type'] = 'billable_fixed'
- # if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
- if so_line.invoice_status == 'invoiced':
- values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id
- elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
- values['timesheet_invoice_type'] = 'billable_fixed'
- timesheet.write(values)
- return True
+ # default values
+ unit_amount = timesheet.unit_amount
+ so_line = timesheet.so_line
+ values = {
+ 'timesheet_revenue': 0.0,
+ 'timesheet_invoice_type': 'non_billable_project' if not timesheet.task_id else 'non_billable',
+ }
+ # set the revenue and billable type according to the product and the SO line
+ if timesheet.task_id and so_line.product_id.type == 'service':
+ # find the analytic account to convert revenue into its currency
+ analytic_account = timesheet.account_id
+ # convert the unit of mesure into hours
+ sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
+ sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
+
+ # calculate the revenue on the timesheet
+ if so_line.product_id.invoice_policy == 'delivery':
+ values['timesheet_revenue'] = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
+ values['timesheet_invoice_type'] = 'billable_time' if so_line.product_id.service_type == 'timesheet' else 'billable_fixed'
+ elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type == 'timesheet':
+ quantity_hour = unit_amount
+ if so_line.product_uom.category_id == timesheet_uom.category_id:
+ quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
+ # compute the total revenue the SO since we are in fixed price
+ total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
+ # compute the total revenue already existing (without the current timesheet line)
+ domain = [('so_line', '=', so_line.id)]
+ if timesheet.ids:
+ domain += [('id', 'not in', timesheet.ids)]
+ analytic_lines = timesheet.search(domain)
+ total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue'))
+ # compute (new) revenue of current timesheet line
+ values['timesheet_revenue'] = min(
+ analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)),
+ total_revenue_so - total_revenue_invoiced
+ )
+ values['timesheet_invoice_type'] = 'billable_fixed'
+ # if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
+ if so_line.invoice_status == 'invoiced':
+ values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id
+ elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
+ values['timesheet_invoice_type'] = 'billable_fixed'
+
+ return values
diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py
index 18330c1d793..c47ed128a8a 100644
--- a/addons/sms/__manifest__.py
+++ b/addons/sms/__manifest__.py
@@ -14,6 +14,10 @@ The service is provided by the In App Purchase Odoo platform.
'data': [
'wizard/send_sms_views.xml',
'views/res_partner_views.xml',
+ 'views/templates.xml',
+ ],
+ 'qweb': [
+ 'static/src/xml/sms_widget.xml',
],
'installable': True,
'auto_install': True,
diff --git a/addons/sms/static/src/js/sms_widget.js b/addons/sms/static/src/js/sms_widget.js
new file mode 100644
index 00000000000..a01ff3753cf
--- /dev/null
+++ b/addons/sms/static/src/js/sms_widget.js
@@ -0,0 +1,120 @@
+odoo.define('sms.sms_widget', function (require) {
+"use strict";
+
+var basicFields = require('web.basic_fields');
+var core = require('web.core');
+var fieldRegistry = require('web.field_registry');
+var framework = require('web.framework');
+
+var FieldText = basicFields.FieldText;
+var QWeb = core.qweb;
+
+/**
+ * SmsWidget is a widget to display a textarea (the body) and a text representing
+ * the number of SMS and the number of characters. This text is computed every
+ * time the user changes the body.
+ */
+var SmsWidget = FieldText.extend({
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.nbrChar = 0;
+ this.nbrSMS = 0;
+ this.encoding = "GSM7";
+ this.tagName = 'div';
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Compute the number of characters and sms
+ * @private
+ */
+ _compute: function () {
+ var content = this._getValue();
+ this.encoding = this._extractEncoding(content);
+ this.nbrChar = content.length;
+ this.nbrChar += (content.match(/\n/g) || []).length;
+ this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
+ this._renderSMS();
+ },
+ /**
+ * Count the number of SMS of the content
+ * @private
+ * @returns {integer} Number of SMS
+ */
+ _countSMS: function () {
+ if (this.nbrChar === 0) {
+ return 0;
+ }
+ if (this.encoding === 'UNICODE') {
+ if (this.nbrChar <= 70) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 67);
+ }
+ if (this.nbrChar <= 160) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 153);
+ },
+ /**
+ * @private
+ * @override
+ */
+ _renderEdit: function () {
+ this.$el.empty();
+ this._prepareInput($('
')).appendTo(this.$el);
+ this.$el.append($(QWeb.render("sms.sms_count", {})));
+ this._compute();
+ },
+ /**
+ * Extract the encoding depending on the characters in the content
+ * @private
+ * @param {String} content Content of the SMS
+ * @returns {String} Encoding of the content (GSM7 or UNICODE)
+ */
+ _extractEncoding: function (content) {
+ if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
+ return 'GSM7';
+ }
+ return 'UNICODE';
+ },
+ /**
+ * Render the number of characters, sms and the encoding.
+ * @private
+ */
+ _renderSMS: function () {
+ this.$('.o_sms_count').text(this.nbrChar + ' / ' + this.nbrSMS + ' SMS (' + this.encoding + ') ');
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _onChange: function () {
+ this._super.apply(this, arguments);
+ this._compute();
+ },
+ /**
+ * @override
+ * @private
+ */
+ _onInput: function () {
+ this._super.apply(this, arguments);
+ this._compute();
+ },
+});
+
+fieldRegistry.add('sms_widget', SmsWidget);
+
+return SmsWidget;
+});
diff --git a/addons/sms/static/src/tests/sms_widget_test.js b/addons/sms/static/src/tests/sms_widget_test.js
new file mode 100644
index 00000000000..b346c5a6f98
--- /dev/null
+++ b/addons/sms/static/src/tests/sms_widget_test.js
@@ -0,0 +1,67 @@
+odoo.define('sms.sms_widget_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ message: {string: "message", type: "text"},
+ },
+ records: [{
+ id: 1,
+ message: "",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('SmsWidget');
+
+ QUnit.test('Sms widgets are correctly rendered', function (assert) {
+ assert.expect(9);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '
',
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.ok(form.$('.o_sms_count').length, "Should have a sms counter");
+ assert.strictEqual(form.$('.o_sms_count').text(), '0 / 0 SMS (GSM7) ', 'Should be "0 / 0 SMS (GSM7) " by default');
+ // GSM-7
+ form.$('.o_input').val("Hello from Odoo").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 / 1 SMS (GSM7) ', 'Should be "15 / 1 SMS (GSM7) " for "Hello from Odoo"');
+ // GSM-7 with \n => this one count as 2 characters
+ form.$('.o_input').val("Hello from Odoo\n").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '17 / 1 SMS (GSM7) ', 'Should be "17 / 1 SMS (GSM7) " for "Hello from Odoo\\n"');
+ // Unicode => ê
+ form.$('.o_input').val("Hêllo from Odoo").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 / 1 SMS (UNICODE) ', 'Should be "15 / 1 SMS (UNICODE) " for "Hêllo from Odoo"');
+ // GSM-7 with 160c
+ var text = Array(161).join('a');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '160 / 1 SMS (GSM7) ', 'Should be "160 / 1 SMS (GSM7) " for 160 x "a"');
+ // GSM-7 with 161c
+ text = Array(162).join('a');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '161 / 2 SMS (GSM7) ', 'Should be "161 / 2 SMS (GSM7) " for 161 x "a"');
+ // Unicode with 70c
+ text = Array(71).join('ê');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '70 / 1 SMS (UNICODE) ', 'Should be "70 / 1 SMS (UNICODE) " for 70 x "ê"');
+ // Unicode with 71c
+ text = Array(72).join('ê');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '71 / 2 SMS (UNICODE) ', 'Should be "71 / 2 SMS (UNICODE) " for 71 x "ê"');
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/sms/static/src/xml/sms_widget.xml b/addons/sms/static/src/xml/sms_widget.xml
new file mode 100644
index 00000000000..9969cf2b3c1
--- /dev/null
+++ b/addons/sms/static/src/xml/sms_widget.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/addons/sms/views/templates.xml b/addons/sms/views/templates.xml
new file mode 100644
index 00000000000..15bf5fc1454
--- /dev/null
+++ b/addons/sms/views/templates.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index 139f542e597..15d284e0879 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -71,8 +71,7 @@ class SendSMS(models.TransientModel):
phone_numbers = []
no_phone_partners = []
for partner in partners:
- default_field_name = 'mobile' if partner.mobile else 'phone'
- number = self._sms_sanitization(partner, self.env.context.get('field_name') or default_field_name)
+ number = self._sms_sanitization(partner, self.env.context.get('field_name') or 'mobile')
if number:
phone_numbers.append(number)
else:
diff --git a/addons/sms/wizard/send_sms_views.xml b/addons/sms/wizard/send_sms_views.xml
index 34746c32825..165272737c3 100644
--- a/addons/sms/wizard/send_sms_views.xml
+++ b/addons/sms/wizard/send_sms_views.xml
@@ -9,7 +9,7 @@
-
+