[MERGE] forward port branch saas-11.4 up to f6ca72b3ce

This commit is contained in:
Christophe Simonis
2018-11-02 10:52:55 +01:00
73 changed files with 766 additions and 181 deletions
+2
View File
@@ -538,6 +538,8 @@ class AccountJournal(models.Model):
@api.constrains('currency_id', 'default_credit_account_id', 'default_debit_account_id')
def _check_currency(self):
if self.currency_id:
if self.currency_id == self.company_id.currency_id:
raise ValidationError(_("Currency field should only be set if the journal's currency is different from the company's. Leave the field blank to use company currency."))
if self.default_credit_account_id and not self.default_credit_account_id.currency_id.id == self.currency_id.id:
raise ValidationError(_('The currency of the journal should be the same than the default credit account.'))
if self.default_debit_account_id and not self.default_debit_account_id.currency_id.id == self.currency_id.id:
@@ -276,7 +276,6 @@
<field name="type">bank</field>
<field name="default_debit_account_id" ref="usd_bnk"/>
<field name="default_credit_account_id" ref="usd_bnk"/>
<field name="currency_id" ref="base.USD"/>
<field name="bank_account_id" ref="bank_account_usd"/>
</record>
<record id="base.main_company" model="res.company">
+2 -1
View File
@@ -456,6 +456,7 @@ class TestPayment(AccountingTestCase):
#check the invoice status
self.assertEqual(invoice.state, 'paid')
def test_payment_and_writeoff_in_other_currency_3(self):
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
# Invoice made in secondary currency for which the rate to the company currency
@@ -536,4 +537,4 @@ class TestPayment(AccountingTestCase):
# Check the move has been posted properly
self.assertEqual(payment.mapped('move_line_ids.move_id.state'), ['posted'], "After bank reconciliation, the payment's account.move should be posted.")
self.assertEqual(payment.mapped('move_line_ids.move_id.date'), stmt_line_date, "After bank reconciliation, the payment's account.move should share the same date as the bank statement.")
self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
self.assertEqual([payment.payment_date], stmt_line_date, "After bank reconciliation, the payment should share the same date as the bank statement.")
+1 -1
View File
@@ -787,7 +787,7 @@ class TestReconciliation(AccountingTestCase):
currency = self.env.user.company_id.currency_id
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id})
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE'})
statement = self.make_payment(invoice, journal, 50)
+19 -13
View File
@@ -607,13 +607,16 @@ class Meeting(models.Model):
event_date = datetime.datetime.now()
use_naive_datetime = self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule
if use_naive_datetime:
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
else:
if not use_naive_datetime:
# Convert the event date to saved timezone (or context tz) as it'll
# define the correct hour/day asked by the user to repeat for recurrence.
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
event_date = event_date.astimezone(timezone)
# The start date is naive
# the timezone will be applied, if necessary, at the very end of the process
# to allow for DST timezone reevaluation
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
# We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to
@@ -629,12 +632,15 @@ class Meeting(models.Model):
else:
if not recurring_date.tzinfo:
recurring_date = pytz.UTC.localize(recurring_date)
recurring_date = recurring_date.astimezone(timezone)
recurring_date = recurring_date.astimezone(timezone).replace(tzinfo=None)
if date_field == "stop":
recurring_date += timedelta(hours=self.duration)
rset1.exdate(recurring_date)
invalidate = True
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR]
def naive_tz_to_utc(d):
return timezone.localize(d).astimezone(pytz.UTC)
return [naive_tz_to_utc(d) if not use_naive_datetime else d for d in rset1 if d.year < MAXYEAR]
@api.multi
def _get_recurrency_end_date(self):
@@ -893,17 +899,17 @@ class Meeting(models.Model):
def _inverse_dates(self):
for meeting in self:
if meeting.allday:
tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
# Convention break:
# stop and start are NOT in UTC in allday event
# in this case, they actually represent a date
# i.e. Christmas is on 25/12 for everyone
# even if people don't celebrate it simultaneously
enddate = fields.Datetime.from_string(meeting.stop_date)
enddate = tz.localize(enddate)
enddate = enddate.replace(hour=18)
enddate = enddate.astimezone(pytz.utc)
startdate = fields.Datetime.from_string(meeting.start_date)
startdate = tz.localize(startdate) # Add "+hh:mm" timezone
startdate = startdate.replace(hour=8) # Set 8 AM in localtime
startdate = startdate.astimezone(pytz.utc) # Convert to UTC
startdate = startdate.replace(hour=8) # Set 8 AM
meeting.write({
'start': startdate.replace(tzinfo=None),
+39
View File
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, time
from odoo import fields
from odoo.tests.common import TransactionCase
import pytz
class TestCalendar(TransactionCase):
@@ -275,3 +276,41 @@ class TestCalendar(TransactionCase):
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
def test_event_allday(self):
self.env.user.tz = 'Pacific/Honolulu'
event = self.CalendarEvent.create({
'name': 'All Day',
'start': "2018-10-16 00:00:00",
'start_date': "2018-10-16",
'start_datetime': False,
'stop': "2018-10-18 00:00:00",
'stop_date': "2018-10-18",
'stop_datetime': False,
'allday': True,
})
self.assertEqual(str(event.start), '2018-10-16 08:00:00')
self.assertEqual(str(event.stop), '2018-10-18 18:00:00')
def test_recurring_around_dst(self):
m = self.CalendarEvent.create({
'name': "wheee",
'start': '2018-10-27 14:30:00',
'allday': False,
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=4',
'duration': 2,
'stop': '2018-10-27 16:30:00',
})
start_recurring_dates = m.with_context({'tz': 'Europe/Brussels'})._get_recurrent_date_by_event()
self.assertEqual(len(start_recurring_dates), 4)
for d in start_recurring_dates:
self.assertEqual(d.tzinfo, pytz.UTC)
if d.day < 28: # DST switch happens between 2018-10-27 and 2018-10-28
self.assertEqual(d.hour, 14)
else:
self.assertEqual(d.hour, 15)
self.assertEqual(d.minute, 30)
+1 -1
View File
@@ -77,7 +77,7 @@ class HrAttendance(models.Model):
('employee_id', '=', attendance.employee_id.id),
('check_out', '=', False),
('id', '!=', attendance.id),
])
], order='check_in desc', limit=1)
if no_check_out_attendances:
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s") % {
'empl_name': attendance.employee_id.name,
+5 -1
View File
@@ -11,8 +11,12 @@ import werkzeug.urls
def _is_l10n_ch_postal(account_ref):
""" Returns True iff the string account_ref is a valid postal account number,
i.e. it only contains ciphers and is last cipher is the result of a recursive
modulo 10 operation ran over the rest of it.
modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
"""
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', account_ref or ''):
ref_subparts = account_ref.split('-')
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6,'0') + ref_subparts[2]
if re.match('\d+$', account_ref or ''):
account_ref_without_check = account_ref[:-1]
return mod10r(account_ref_without_check) == account_ref
+1 -1
View File
@@ -1807,7 +1807,7 @@
</record>
<record id="pcg_4661" model="account.account.template">
<field name="name">>Datorii din operaţiuni de fiducie</field>
<field name="name">Datorii din operaţiuni de fiducie</field>
<field name="code">4661</field>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="ro_chart_template"/>
+11 -13
View File
@@ -81,8 +81,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ
</field>
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ</field>
</record>
<record id="chart1332" model="account.account.template">
<field name="code">1332</field>
@@ -140,7 +139,7 @@
</record>
<record id="chart1388" model="account.account.template">
<field name="code">1388</field>
<field name="reconcile" eval="False"/>
<field name="reconcile" eval="True"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Phải thu khác</field>
@@ -149,7 +148,7 @@
<record id="chart141" model="account.account.template">
<field name="code">141</field>
<field name="reconcile" eval="False"/>
<field name="reconcile" eval="True"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Tạm ứng</field>
@@ -322,8 +321,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm
</field>
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm</field>
</record>
<record id="chart2118" model="account.account.template">
<field name="code">2118</field>
@@ -449,7 +447,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">>Đầu tư vào công ty liên doanh</field>
<field name="name">Đầu tư vào công ty liên doanh</field>
</record>
@@ -459,7 +457,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">>Đầu tư góp vốn vào đơn vị khác</field>
<field name="name">Đầu tư góp vốn vào đơn vị khác</field>
</record>
<record id="chart2288" model="account.account.template">
@@ -651,14 +649,14 @@
<record id="chart3341" model="account.account.template">
<field name="code">3341</field>
<field name="reconcile" eval="False"/>
<field name="reconcile" eval="True"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Phải trả công nhân viên</field>
</record>
<record id="chart3348" model="account.account.template">
<field name="code">3348</field>
<field name="reconcile" eval="False"/>
<field name="reconcile" eval="True"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Phải trả người lao động khác</field>
@@ -677,7 +675,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Phải trả nội bộ về vốn kinh doanh </field>
<field name="name">Phải trả nội bộ về vốn kinh doanh</field>
</record>
<record id="chart3362" model="account.account.template">
@@ -763,7 +761,7 @@
</record>
<record id="chart3388" model="account.account.template">
<field name="code">3388</field>
<field name="reconcile" eval="False"/>
<field name="reconcile" eval="True"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Phải trả, phải nộp khác</field>
@@ -790,7 +788,7 @@
<field name="reconcile" eval="False"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Trái phiếu </field>
<field name="name">Trái phiếu</field>
</record>
<record id="chart34311" model="account.account.template">
<field name="code">34311</field>
@@ -9,6 +9,7 @@ from odoo import api, fields, models, tools
class MailTracking(models.Model):
_name = 'mail.tracking.value'
_description = 'Mail Tracking Value'
_rec_name = 'field'
_order = 'track_sequence asc'
# TDE CLEANME: why not a m2o to ir model field ?
@@ -390,6 +390,7 @@ var BasicComposer = Widget.extend({
return;
}
this.$(".o_composer_button_send").prop("disabled", true);
clearTimeout(this._cannedTimeout);
var self = this;
this._preprocessMessage().then(function (message) {
+1 -3
View File
@@ -351,6 +351,7 @@
</span>
</p>
<div class="o_thread_message_content">
<t t-raw="message.getBody()"/>
<t t-if="message.hasTrackingValues()">
<t t-if="message.hasSubtypeDescription()">
<p><t t-esc="message.getSubtypeDescription()"/></p>
@@ -358,9 +359,6 @@
<t t-call="mail.widget.Thread.MessageTracking"/>
</t>
<p t-if="options.displaySubjectOnMessages and message.shouldDisplaySubject()" class="o_mail_subject">Subject: <t t-esc="message.getSubject()"/></p>
<t t-if="!(message.hasTrackingValues())">
<t t-raw="message.getBody()"/>
</t>
<t t-if="message.hasAttachments()">
<div t-if="message.hasImageAttachments()" class="o_attachments_previews">
<t t-foreach="message.getImageAttachments()" t-as="attachment">
+53
View File
@@ -786,6 +786,59 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
});
});
QUnit.test('chatter: post a message disable the send button', function(assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (args.method === 'message_get_suggested_recipients') {
return $.when({2: []});
}
if (args.method === 'message_post') {
assert.ok(form.$('.o_composer_button_send').prop("disabled"),
"Send button should be disabled when a message is being sent");
return $.when(57923);
}
if (args.method === 'message_format') {
return $.when([{
author_id: ["42", "Me"],
model: 'partner',
}]);
}
return this._super(route, args);
},
intercepts: {
get_messages: function (ev) {
ev.stopPropagation();
ev.data.callback($.when([]));
},
},
});
form.$('.o_chatter_button_new_message').click();
assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
"Send button should be enabled when posting a message");
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
form.$('.oe_chatter .o_composer_button_send').click();
form.$('.o_chatter_button_new_message').click();
assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
"Send button should be enabled when posting another message");
form.destroy();
});
QUnit.test('chatter: receive notif when document is open', function (assert) {
assert.expect(2);
+1 -1
View File
@@ -123,7 +123,7 @@ class MrpUnbuild(models.Model):
if produce_move.has_tracking != 'none':
original_move = self.mo_id.move_raw_ids.filtered(lambda move: move.product_id == produce_move.product_id)
needed_quantity = produce_move.product_qty
for move_lines in original_move.mapped('move_line_ids'):
for move_lines in original_move.mapped('move_line_ids').filtered(lambda ml: ml.lot_produced_id == self.lot_id):
# Iterate over all move_lines until we unbuilded the correct quantity.
taken_quantity = min(needed_quantity, move_lines.qty_done)
if taken_quantity:
@@ -13,7 +13,7 @@
<div class="row mt16 o_settings_container">
<div class="col-lg-6 col-12 o_setting_box" id="work_order" title="Work Order Operations allow you to create and manage the manufacturing operations that should be followed within your work centers in order to produce a product. They are attached to bills of materials that will define the required raw materials.">
<div class="o_setting_left_pane">
<field name="group_mrp_routings" widget="upgrade_boolean"/>
<field name="group_mrp_routings"/>
<field name="module_mrp_workorder" invisible="1"/>
</div>
<div class="o_setting_right_pane" id="workorder_settings">
+1 -2
View File
@@ -53,12 +53,11 @@ class ProductProduct(models.Model):
if not boms_to_recompute:
boms_to_recompute = []
total = 0
quant_quantity = bom.product_uom_id._compute_quantity(bom.product_qty, bom.product_tmpl_id.uom_id)
for opt in bom.routing_id.operation_ids:
duration_expected = (
opt.workcenter_id.time_start +
opt.workcenter_id.time_stop +
quant_quantity * opt.time_cycle)
opt.time_cycle)
total += (duration_expected / 60) * opt.workcenter_id.costs_hour
for line in bom.bom_line_ids:
if line._skip_bom_line(self):
+7 -7
View File
@@ -3,7 +3,7 @@
from odoo.exceptions import UserError
from odoo.tests import common, Form
from odoo.tools.float_utils import float_round
from odoo.tools.float_utils import float_round, float_compare
class TestBom(common.TransactionCase):
@@ -166,11 +166,11 @@ class TestBom(common.TransactionCase):
# Table Head Operation Cost (1 Dozen)
# --------------------------------------------------------------------------
# Operation cost calculate for 1 dozens
# Cutting (20 * 12 / 60) * 100 = 400
# Drilling (25 * 12 / 60) * 100 = 500
# Fitting (30 * 12 / 60) * 100 = 600
# Cutting (20 * 1 / 60) * 100 = 33,33
# Drilling (25 * 1 / 60) * 100 = 41,67
# Fitting (30 * 1 / 60) * 100 = 50
# ----------------------------------------
# Operation Cost 1 dozen (1500 per dozen) and 125 for 1 Unit
# Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
# --------------------------------------------------------------------------
self.bom_2.routing_id = routing_1.id
@@ -180,8 +180,8 @@ class TestBom(common.TransactionCase):
# Total cost of Dining Table = (550) + Total cost of operations (125) = 675.0
self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 675.0, "After computing price from BoM price should be 612.5")
self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost()
# Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 125) = 968.75
self.assertEquals(float_round(self.dining_table.standard_price, precision_digits=2), 968.75, "After computing price from BoM price should be 786.46")
# Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 10.42) = 854.17
self.assertEquals(float_compare(self.dining_table.standard_price, 854.17, precision_digits=2), 0, "After computing price from BoM price should be 786.46")
def test_01_compute_price_inventory_valuation(self):
"""Test update cost from bom in list view when inventory valuation is real time."""
+1 -1
View File
@@ -28,7 +28,7 @@ class StripeController(http.Controller):
try:
acquirer.s2s_process(post)
except Exception as e:
error = e.message
error = str(e)
return_url = post.get('return_url', '/')
if error:
@@ -2170,6 +2170,19 @@ td {
overflow: auto;
height: 120px;
}
.pos .modal-dialog .popup-textinput{
display: flex;
flex-direction: column;
}
.pos .modal-dialog .popup-textinput .title,
.pos .modal-dialog .popup-textinput .footer{
flex: none;
}
.pos .modal-dialog .popup-textinput textarea{
resize: none;
flex: auto;
margin: 0 20px 80px;
}
.pos .popup .title {
background: rgba(255,255,255,0.5);
margin: 0;
@@ -741,6 +741,9 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
if(err.message === 'XmlHttpRequestError '){
title = 'Network Failure (XmlHttpRequestError)';
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'TLSError'){
title = 'Https connection to IoT Box failed';
body = 'Make sure you are using IoT Box v18.10 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
}else if(err.code === 200){
title = err.data.message;
body = err.data.debug;
+12 -7
View File
@@ -192,11 +192,15 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
// try harder when we remember a good proxy url
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
.then(null,function(){
return self.find_proxy(options);
if (window.location.protocol != 'https:'){
return self.find_proxy(options);
}
});
}else{
// just find something quick
found_url = this.find_proxy(options);
if (window.location.protocol != 'https:'){
found_url = this.find_proxy(options);
}
}
success = found_url.then(function(url){
@@ -248,16 +252,17 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
// try several time to connect to a known proxy url
try_hard_to_connect: function(url,options){
options = options || {};
var port = ':' + (options.port || '8069');
var protocol = window.location.protocol;
var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069');
this.set_connection_status('connecting');
if(url.indexOf('//') < 0){
url = 'http://'+url;
url = protocol + '//' + url;
}
if(url.indexOf(':',5) < 0){
url = url+port;
url = url + port;
}
// try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
@@ -273,11 +278,11 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
.done(function(){
done.resolve(url);
})
.fail(function(){
.fail(function(resp){
if(retries > 0){
try_real_hard_to_connect(url,retries-1,done);
}else{
done.reject();
done.reject(resp.statusText, url);
}
});
return done;
+15 -7
View File
@@ -127,13 +127,21 @@ exports.PosModel = Backbone.Model.extend({
progress: function(prog){
self.chrome.loading_progress(prog);
},
}).then(function(){
if(self.config.iface_scan_via_proxy){
self.barcode_reader.connect_to_proxy();
}
}).always(function(){
done.resolve();
});
}).then(
function(){
if(self.config.iface_scan_via_proxy){
self.barcode_reader.connect_to_proxy();
}
done.resolve();
},
function(statusText, url){
if (statusText == 'error' && window.location.protocol == 'https:') {
var error = {message: 'TLSError', url: url};
self.chrome.loading_error(error);
} else {
done.resolve();
}
});
return done;
},
@@ -1289,6 +1289,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
} else {
fields.property_product_pricelist = false;
}
var contents = this.$(".client-details-contents");
contents.off("click", ".button.save");
rpc.query({
model: 'res.partner',
@@ -1308,6 +1311,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
'title': _t('Error: Could not Save Changes'),
'body': error_body,
});
contents.on('click','.button.save',function(){ self.save_client_details(partner); });
});
},
@@ -1325,6 +1329,8 @@ var ClientListScreenWidget = ScreenWidget.extend({
// has created, and reload_partner() must have loaded the newly created partner.
self.display_client_details('hide');
}
}).always(function(){
$(".client-details-contents").on('click','.button.save',function(){ self.save_client_details(partner); });
});
},
@@ -259,7 +259,8 @@ var TableWidget = PosBaseWidget.extend({
args: [[['id', '=', table_id]], fields],
limit: 1,
})
.then(function (table){
.then(function (result){
var table = result[0];
for (var field in table) {
self.table[field] = table[field];
}
@@ -44,7 +44,7 @@ class PurchaseRequisition(models.Model):
_order = "id desc"
def _get_picking_in(self):
pick_in = self.env.ref('stock.picking_type_in')
pick_in = self.env.ref('stock.picking_type_in', raise_if_not_found=False)
company = self.env['res.company']._company_default_get('purchase.requisition')
if not pick_in or pick_in.sudo().warehouse_id.company_id.id != company.id:
pick_in = self.env['stock.picking.type'].search(
@@ -120,7 +120,7 @@ class AccountInvoice(models.Model):
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
# be processed automatically by the rest of the code.
diff_res.append({
diff_line = {
'type': 'src',
'name': i_line.name[:64],
'price_unit': inv.currency_id.round(price_unit_val_dif),
@@ -131,7 +131,11 @@ class AccountInvoice(models.Model):
'uom_id': line['uom_id'],
'account_analytic_id': line['account_analytic_id'],
'tax_ids': tax_ids,
})
}
# We update the original line accordingly
line['price_unit'] = inv.currency_id.round(line['price_unit'] - diff_line['price_unit'])
line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit'])
diff_res.append(diff_line)
return diff_res
return []
@@ -25,7 +25,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
})
self.test_product_category.property_account_creditor_price_difference_categ = self.price_dif_account.id
def _create_purchase(self, product, quantity=1.0):
def _create_purchase(self, product, date, quantity=1.0):
rslt = self.env['purchase.order'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
@@ -36,14 +36,14 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'product_qty': quantity,
'product_uom': product.uom_po_id.id,
'price_unit': self.product_price_unit,
'date_planned': '2017-10-07',
'date_planned': date,
})],
'date_order': '2017-10-01',
'date_order': date,
})
rslt.button_confirm()
return rslt
def _create_invoice_for_po(self, purchase_order):
def _create_invoice_for_po(self, purchase_order, date):
account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
rslt = self.env['account.invoice'].create({
'purchase_id': purchase_order.id,
@@ -51,8 +51,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'currency_id': self.currency_two.id,
'name': 'vendor bill',
'type': 'in_invoice',
'date_invoice': '2017-12-31',
'date': '2017-12-31',
'date_invoice': date,
'date': date,
'account_id': account_receivable.id,
})
rslt.purchase_order_change()
@@ -62,11 +62,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive the goods first, and then make the invoice.
"""
test_product = self.test_product_delivery
purchase_order = self._create_purchase(test_product)
purchase_order = self._create_purchase(test_product, '2018-01-01')
self._process_pickings(purchase_order.picking_ids)
invoice = self._create_invoice_for_po(purchase_order)
self.currency_rate.rate = 7.76435463
invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2018-02-01',
})
invoice.action_invoice_open()
picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
self.check_reconciliation(invoice, picking)
@@ -77,29 +82,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
def test_invoice_shipment(self):
""" Tests the case into which we make the invoice first, and then receive the goods.
"""
# First, we delete the currency rate defined in demo data for USD on June 6th.
# This is mandatory to ensure consistency of the data generated by the test,
# as stock pickings created from a PO are by design always created for
# the current date (there is no way forcing this), meaning that they
# always use today's exchange rate for their valuation.
# Despite the fact we rewrite the date of the valuation moves artificially,
# we cannot correct the debit and credit values, since the anglosaxon
# entries get automatically reconciled (and you cannot modify a reconciled entry).
# So, we have to make sure that "today"'s rate will always be the last rate we
# created in order to ensure complete control of the test.
self.env.ref('base.rateUSDbis').unlink()
# Create a PO and an invoice for it
test_product = self.test_product_order
purchase_order = self._create_purchase(test_product)
purchase_order = self._create_purchase(test_product, '2017-12-01')
invoice = self._create_invoice_for_po(purchase_order)
invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 1
# The currency rate changes
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
@@ -113,7 +105,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.check_reconciliation(invoice, picking)
# The currency rate changes again
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 10.54739702,
@@ -132,7 +124,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self._change_pickings_date(return_pick, '2018-01-13')
# The currency rate changes again
self.currency_rate = self.env['res.currency.rate'].create({
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.56564564,
@@ -157,25 +149,40 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
""" Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
"""
test_product = self.test_product_delivery
purchase_order = self._create_purchase(test_product, quantity=5.0)
purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0)
self._process_pickings(purchase_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_po(purchase_order)
invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 3
self.currency_rate.rate = 7.76435463
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2017-02-01',
})
invoice.action_invoice_open()
self.check_reconciliation(invoice, picking, full_reconcile=False)
invoice2 = self._create_invoice_for_po(purchase_order)
invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
invoice_line.quantity = 2
self.currency_rate.rate = 13.834739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
'name': '2017-03-01',
})
invoice2.action_invoice_open()
self.check_reconciliation(invoice2, picking, full_reconcile=False)
self.currency_rate.rate = 12.195747002
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 12.195747002,
'name': '2017-04-01',
})
self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking)
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock.tests.common import TestStockCommon
class TestReplenishWizard(TestStockCommon):
def setUp(self):
super(TestReplenishWizard, self).setUp()
self.vendor = self.env['res.partner'].search([('supplier', '=', True)], limit=1)
self.vendor = self.env['res.partner'].create(dict(name='The Replenisher', supplier=True))
self.product1_price = 500
# Create a supplier info witch the previous vendor
@@ -5,6 +5,7 @@ import time
from datetime import datetime
from odoo import fields
from odoo.tests import Form
from odoo.tests.common import TransactionCase, tagged
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
@@ -417,3 +418,52 @@ class TestStockValuationWithCOA(AccountingTestCase):
# check the anglo saxon entries
price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)])
self.assertEqual(price_diff_entry.credit, 100)
def test_anglosaxon_valuation(self):
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.product_tmpl_id.cost_method = 'fifo'
self.product1.product_tmpl_id.valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
price_diff_account = self.env['account.account'].create({
'name': 'price diff account',
'code': 'price diff account',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
})
self.product1.property_account_creditor_price_difference = price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 1
po_line.price_unit = 10.0
order = po_form.save()
order.button_confirm()
# Receive the goods
receipt = order.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create an invoice with a different price
invoice = self.env['account.invoice'].create({
'partner_id': order.partner_id.id,
'purchase_id': order.id,
'account_id': order.partner_id.property_account_payable_id.id,
'type': 'in_invoice',
})
invoice.purchase_order_change()
invoice.invoice_line_ids[0].price_unit = 15.0
invoice.action_invoice_open()
# Check what was posted in the price difference account
price_diff_aml = self.env['account.move.line'].search([('account_id','=',price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
self.assertAlmostEquals(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)")
# Check what was posted in stock input account
input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 10, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.")
+2
View File
@@ -39,6 +39,8 @@ KanbanRecord.include({
.done(function() {
self.trigger_up('kanban_record_update', {id: self.id});
});
// TODO: previous lines can be refactored as follows (in master)
// self.trigger_up('kanban_record_update', {invoiced_target: value});
}
});
},
@@ -0,0 +1,70 @@
odoo.define('sale.dashboard_tests', function (require) {
"use strict";
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('Sales Team Dashboard', {
beforeEach: function () {
this.data = {
'crm.team': {
fields: {
foo: {string: "Foo", type: 'char'},
invoiced_target: {string: "Invoiced_target", type: 'integer'},
},
records: [
{id: 1, foo: "yop"},
],
},
};
}
});
QUnit.test('edit target with several o_kanban_primary_bottom divs', function (assert) {
assert.expect(4);
var kanban = createView({
View: KanbanView,
model: 'crm.team',
data: this.data,
arch: '<kanban>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<div class="container o_kanban_card_content">' +
'<a href="#" class="sales_team_target_definition o_inline_link">' +
'Click to define a target</a>' +
'<div class="col-12 o_kanban_primary_bottom"/>' +
'<div class="col-12 o_kanban_primary_bottom bottom_block"/>' +
'</div>' +
'</t>' +
'</templates>' +
'</kanban>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.strictEqual(args.args[1].invoiced_target, 123,
"new value is correctly saved");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(kanban.$('.o_kanban_view .sales_team_target_definition').length, 1,
"should have classname 'sales_team_target_definition'");
assert.strictEqual(kanban.$('.o_kanban_primary_bottom').length, 2,
"should have two divs with classname 'o_kanban_primary_bottom'");
kanban.$('a.sales_team_target_definition').click();
assert.strictEqual(kanban.$('.o_kanban_primary_bottom:last input').length, 1,
"should have rendered an input in the last o_kanban_primary_bottom div");
kanban.$('.o_kanban_primary_bottom:last input').focus();
kanban.$('.o_kanban_primary_bottom:last input').val('123');
kanban.$('.o_kanban_primary_bottom:last input').blur();
kanban.destroy();
});
});
+6
View File
@@ -28,4 +28,10 @@
<script type="text/javascript" src="/sale/static/tests/product_configurator.test.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="sales_team_dashboard_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/sale/static/tests/sales_team_dashboard_tests.js"></script>
</xpath>
</template>
</odoo>
@@ -30,7 +30,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
#set the invoice_policy to delivery to have an accurate COGS entry
self.test_product_delivery.invoice_policy = "delivery"
def _create_sale(self, product, quantity=1.0):
def _create_sale(self, product, date, quantity=1.0):
rslt = self.env['sale.order'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
@@ -42,17 +42,18 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
'product_uom': product.uom_po_id.id,
'price_unit': self.product_price_unit,
})],
'date_order': date,
})
rslt.action_confirm()
return rslt
def _create_invoice_for_so(self, sale_order, product):
def _create_invoice_for_so(self, sale_order, product, date):
rslt = self.env['account.invoice'].create({
'partner_id': self.test_partner.id,
'currency_id': self.currency_two.id,
'name': 'customer invoice',
'type': 'out_invoice',
'date_invoice': time.strftime('%Y') + '-12-22',
'date_invoice': date,
'account_id': self.account_receivable.id,
'invoice_line_ids': [(0, 0, {
'name': 'test line',
@@ -92,11 +93,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product)
sale_order = self._create_sale(test_product, '2108-01-01')
self._process_pickings(sale_order.picking_ids)
invoice = self._create_invoice_for_so(sale_order, test_product)
self.currency_rate.rate = 9.87366352
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.87366352,
'name': '2018-02-01',
})
invoice.action_invoice_open()
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
self.check_reconciliation(invoice, picking, operation='sale')
@@ -110,10 +116,15 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.test_product_delivery.standard_price = 13
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product)
sale_order = self._create_sale(test_product, '2018-01-01')
invoice = self._create_invoice_for_so(sale_order, test_product)
self.currency_rate.rate = 0.974784
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 0.974784,
'name': '2018-02-01',
})
invoice.action_invoice_open()
self._process_pickings(sale_order.picking_ids)
@@ -122,7 +133,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
self.check_reconciliation(invoice, picking, operation='sale')
#return the goods and refund the invoice
self.currency_rate.rate = 10.54739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 10.54739702,
'name': '2018-03-01',
})
stock_return_picking = self.env['stock.return.picking']\
.with_context(active_ids=[picking.id], active_id=picking.id).create({})
stock_return_picking.product_return_moves.quantity = 1.0
@@ -131,7 +147,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
return_pick.action_assign()
return_pick.move_lines.quantity_done = 1
return_pick.action_done()
self.currency_rate.rate = 9.56564564
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 9.56564564,
'name': '2018-04-01',
})
refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({
'description': 'test_invoice_shipment_refund',
'filter_refund': 'cancel',
@@ -147,26 +168,41 @@ class TestValuationReconciliation(ValuationReconciliationTestCase):
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
sale_order = self._create_sale(test_product, quantity=5)
sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
self._process_pickings(sale_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_so(sale_order, test_product)
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)])
invoice_line.quantity = 3
self.currency_rate.rate = 7.76435463
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 7.76435463,
'name': '2018-02-01',
})
invoice.action_invoice_open()
self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale')
invoice2 = self._create_invoice_for_so(sale_order, test_product)
invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12')
invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)])
invoice_line.quantity = 2
self.currency_rate.rate = 13.834739702
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 13.834739702,
'name': '2018-03-01',
})
invoice2.action_invoice_open()
self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale')
self.currency_rate.rate = 12.195747002
self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 12.195747002,
'name': '2018-04-01',
})
self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking, operation='sale')
+4 -2
View File
@@ -361,8 +361,10 @@ class ProcurementGroup(models.Model):
self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id)
# Search all confirmed stock_moves and try to assign them
confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc')
for moves_chunk in split_every(100, confirmed_moves.ids):
moves_to_assign = self.env['stock.move'].search([
('state', 'in', ['confirmed', 'partially_available']), ('product_uom_qty', '!=', 0.0)
], limit=None, order='priority desc, date_expected asc')
for moves_chunk in split_every(100, moves_to_assign.ids):
self.env['stock.move'].browse(moves_chunk)._action_assign()
if use_new_cursor:
self._cr.commit()
+3 -1
View File
@@ -332,9 +332,11 @@
<field name="group_id" groups="base.group_no_one"/>
<field name="priority" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
</group>
<field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
</group>
</page>
<page string="Note" name="note">
<field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
@@ -58,12 +58,6 @@ class ValuationReconciliationTestCase(AccountingTestCase):
self.currency_one = self.company.currency_id
currency_two_name = 'USD' if self.currency_one.name != 'USD' else 'EUR'
self.currency_two = self.env['res.currency'].search([('name', '=', currency_two_name)])
self.currency_rate = self.env['res.currency.rate'].create({
'currency_id': self.currency_one.id,
'company_id': self.company.id,
'rate': 1.234343354, # Totally arbitratry value
'name': '2017-01-01',
})
self.input_account = self.env['account.account'].create({
'name': 'Test stock in',
@@ -127,3 +121,15 @@ class ValuationReconciliationTestCase(AccountingTestCase):
})
self.product_price_unit = 66.0
# We delete the currency rate defined in demo data for USD on June 6th.
# This is mandatory to ensure consistency of the data generated by the test,
# as stock pickings created from a PO are by design always created for
# the current date (there is no way forcing this), meaning that they
# always use today's exchange rate for their valuation.
# Despite the fact we rewrite the date of the valuation moves artificially,
# we cannot correct the debit and credit values, since the anglosaxon
# entries get automatically reconciled (and you cannot modify a reconciled entry).
# So, we have to make sure that "today"'s rate will always be the last rate we
# created in order to ensure complete control of the test.
self.env.ref('base.rateUSDbis').unlink()
@@ -2188,7 +2188,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string[]} [options.fieldNames] the list of fields to fetch. If
* not given, fetch all the fields in record.fieldNames (+ display_name)
* @param {string} [optinos.viewType] the type of view for which the record
* @param {string} [options.viewType] the type of view for which the record
* is fetched (usefull to load the adequate fields), by defaults, uses
* record.viewType
* @returns {Deferred<Object>} resolves to the record or is rejected in
@@ -3785,11 +3785,12 @@ var BasicModel = AbstractModel.extend({
*/
_postprocess: function (record, options) {
var self = this;
var viewType = options && options.viewType || record.viewType;
var defs = [];
_.each(record.getFieldNames(options), function (name) {
var field = record.fields[name];
var fieldInfo = record.fieldsInfo[record.viewType][name] || {};
var fieldInfo = record.fieldsInfo[viewType][name] || {};
var options = fieldInfo.options || {};
if (options.always_reload) {
if (record.fields[name].type === 'many2one' && record.data[name]) {
@@ -3798,7 +3799,7 @@ var BasicModel = AbstractModel.extend({
model: field.relation,
method: 'name_get',
args: [element.data.id],
context: self._getContext(record, {fieldName: name}),
context: self._getContext(record, {fieldName: name, viewType: viewType}),
})
.then(function (result) {
element.data.display_name = result[0][1];
@@ -150,6 +150,14 @@ var BasicView = AbstractView.extend({
}
}
}
// Many2one: context is not the same between the different views
// this means the result of a name_get could differ
if (fieldType === 'many2one') {
if (JSON.stringify(record.data[name].context) !==
JSON.stringify(fieldInfo.context)) {
fieldNames.push(name);
}
}
}
});
@@ -273,7 +273,7 @@ var KanbanController = BasicController.extend({
_onButtonNew: function () {
var state = this.model.get(this.handle, {raw: true});
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
if (this.on_create === 'quick_create' && quickCreateEnabled) {
if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) {
// Activate the quick create in the first column
this.renderer.addQuickCreate();
} else if (this.on_create && this.on_create !== 'quick_create') {
@@ -2332,7 +2332,7 @@ QUnit.module('basic_fields', {
var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
$('.discreteBar')[0].dispatchEvent(evt);
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText;
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText.trim();
assert.equal(tooltip, graph_key, "graph tooltip should be generated ");
$('.nvtooltip').remove();
@@ -380,6 +380,93 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('show_address works in a view embedded in a view of another type', function (assert) {
assert.expect(1);
this.data.turtle.records[1].turtle_trululu = 2;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'<field name="turtles"/>' +
'</form>',
res_id: 1,
archs: {
"turtle,false,form": '<form string="T">' +
'<field name="display_name"/>' +
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
'</form>',
"turtle,false,list": '<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>',
},
mockRPC: function (route, args) {
if (args.method === 'name_get') {
return this._super(route, args).then(function (result) {
if (args.model === 'partner' && args.kwargs.context.show_address) {
result[0][1] += '\nrue morgue\nparis 75013';
}
return result;
});
}
return this._super(route, args);
},
});
// click the turtle field, opens a modal with the turtle form view
form.$('.o_data_row:first td.o_data_cell').click();
assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013",
"The partner's address should be displayed");
form.destroy();
});
QUnit.test('many2one data is reloaded if there is a context to take into account', function (assert) {
assert.expect(1);
this.data.turtle.records[1].turtle_trululu = 2;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'<field name="turtles"/>' +
'</form>',
res_id: 1,
archs: {
"turtle,false,form": '<form string="T">' +
'<field name="display_name"/>' +
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
'</form>',
"turtle,false,list": '<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="turtle_trululu"/>' +
'</tree>',
},
mockRPC: function (route, args) {
if (args.method === 'name_get') {
return this._super(route, args).then(function (result) {
if (args.model === 'partner' && args.kwargs.context.show_address) {
result[0][1] += '\nrue morgue\nparis 75013';
}
return result;
});
}
return this._super(route, args);
},
});
// click the turtle field, opens a modal with the turtle form view
form.$('.o_data_row:first').click();
assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013",
"The partner's address should be displayed");
form.destroy();
});
QUnit.test('many2ones in form views with search more', function (assert) {
assert.expect(3);
this.data.partner.records.push({
@@ -4737,6 +4737,30 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick_create on grouped kanban without column', function (assert) {
assert.expect(1);
this.data.partner.records = [];
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test" on_create="quick_create"><templates><t t-name="kanban-box">' +
'<div>' +
'<field name="name"/>' +
'</div>' +
'</t></templates></kanban>',
groupBy: ['product_id'],
intercepts: {
switch_view: function (event) {
assert.ok(true, "switch_view was called instead of quick_create");
},
},
});
kanban.$buttons.find('.o-kanban-button-new').click();
kanban.destroy();
});
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
assert.expect(3);
@@ -611,6 +611,14 @@ var RTEWidget = Widget.extend({
return;
}
// Removes strange _moz_abspos attribute when it appears. Cannot
// find another solution which works in all cases. A grabber still
// appears at the same time which I did not manage to remove.
// TODO find a complete and better solution
_.defer(function () {
$editable.find('[_moz_abspos]').removeAttr('_moz_abspos');
});
if ($target.is('a')) {
/**
* Remove content editable everywhere and add it on the link only so that characters can be added
@@ -280,15 +280,36 @@ var SnippetOption = Widget.extend({
*/
_setActive: function () {
var self = this;
this.$el.find('[data-toggle-class], [data-select-class]')
.addBack('[data-toggle-class], [data-select-class]')
this.$el.find('[data-toggle-class]')
.addBack('[data-toggle-class]')
.removeClass('active')
.filter(function () {
var $elem = $(this);
var className = $elem.data('toggleClass') || $elem.data('selectClass');
return self.$target.hasClass(className);
var className = $(this).data('toggleClass');
return !className || self.$target.hasClass(className);
})
.addClass('active');
_processSelectClassElements(this.$el);
_.each(this.$el.find('.dropdown-menu'), function (group) {
_processSelectClassElements($(group).children());
});
function _processSelectClassElements($elements) {
var maxNbClasses = -1;
$elements.filter('[data-select-class]')
.removeClass('active')
.filter(function () {
var className = $(this).data('selectClass');
var nbClasses = className ? className.split(' ').length : 0;
if (nbClasses >= maxNbClasses && (!className || self.$target.hasClass(className))) {
maxNbClasses = nbClasses;
return true;
}
return false;
})
.last()
.addClass('active');
}
},
//--------------------------------------------------------------------------
@@ -749,7 +770,7 @@ registry.colorpicker = SnippetOption.extend({
*/
_onColorResetButtonClick: function () {
this.$target.removeClass(this.classes).css('background-color', '');
self.$target.trigger('content_changed');
this.$target.trigger('content_changed');
this.$el.find('.colorpicker button.selected').removeClass('selected');
},
});
@@ -852,7 +873,9 @@ registry.background = SnippetOption.extend({
*/
setTarget: function () {
this._super.apply(this, arguments);
// TODO should be automatic for all options as equal to the start method
this.bindBackgroundEvents();
this.__customImageSrc = this._getSrcFromCssValue();
},
//--------------------------------------------------------------------------
+5
View File
@@ -111,6 +111,11 @@ class Website(models.Model):
('b2c', 'Free sign up'),
], string='Customer Account', default='b2b')
@api.onchange('language_ids')
def _onchange_language_ids(self):
if self.language_ids and self.default_lang_id not in self.language_ids:
self.default_lang_id = self.language_ids[0]
@api.multi
def _compute_menu(self):
Menu = self.env['website.menu']
@@ -271,6 +271,7 @@ options.registry.carousel = options.Class.extend({
* @override
*/
_setActive: function () {
this._super.apply(this, arguments);
this.$el.find('[data-interval]').removeClass('active')
.filter('[data-interval=' + this.$target.attr('data-interval') + ']').addClass('active');
},
+7 -1
View File
@@ -392,11 +392,16 @@ class WebsiteSale(ProductConfiguratorController):
@http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False)
def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
sale_order = request.website.sale_get_order(force_create=1)
if sale_order.state != 'draft':
request.session['sale_order_id'] = None
sale_order = request.website.sale_get_order(force_create=True)
product_custom_attribute_values = None
if kw.get('product_custom_attribute_values'):
product_custom_attribute_values = json.loads(kw.get('product_custom_attribute_values'))
request.website.sale_get_order(force_create=1)._cart_update(
sale_order._cart_update(
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
@@ -569,6 +574,7 @@ class WebsiteSale(ProductConfiguratorController):
new_values['customer'] = True
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id
new_values['website_id'] = request.website.id
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
@@ -44,6 +44,8 @@ def tokenize(typespec):
yield (LITERAL, ast.literal_eval(string))
elif toktype == token.ENDMARKER:
return
elif toktype == token.NEWLINE:
pass
else:
raise ValueError("Unknown typespec token %s" % token.tok_name[toktype])
+1 -1
View File
@@ -220,7 +220,7 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
else:
opts = {}
def warner(msg):
def warner(msg, **kw):
self.builder.warn(msg, (self.builder.current_docname, node.line))
highlighted = self.builder.highlighter.highlight_block(
node.rawsource, lang, opts=opts, warn=warner, linenos=linenos,
+15
View File
@@ -0,0 +1,15 @@
The Netherlands, 2018-10-24
Brahoo B.V. agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers
List of contributors:
Cas Vissers c.vissers@brahoo.nl https://github.com/CasVissers
+12
View File
@@ -0,0 +1,12 @@
India, 17, Sept - 2018
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Anil Kesariya anil.r.kesariya@gmail.com https://github.com/AnilKesariya
+11
View File
@@ -0,0 +1,11 @@
Mexico, 2018-10-24
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Luis Guzman ark@switnet.org https://github.com/ark74
+11
View File
@@ -0,0 +1,11 @@
Argentina, 2018-10-18
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Marcelo Ariel Hamra marcelo.hamra@gmail.com https://github.com/mhamra
+3 -3
View File
@@ -1010,17 +1010,17 @@ them (e.g. to change their default sort order):
.. literalinclude:: ../../odoo/addons/test_documentation_examples/extension.py
:language: python
:lines: 5-
:lines: 7-
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py
:language: python
:lines: 8,13
:lines: 10,15
will yield:
.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py
:language: text
:lines: 11
:lines: 13
.. note:: it will also yield the various :ref:`automatic fields
<reference/orm/model/automatic>` unless they've been disabled
+66 -22
View File
@@ -81,8 +81,11 @@ Overview
The Credits
.. note:: The credits went from integer to float value starting **October 2018**
Integer values are still supported.
Every service provided through the In-App platform can be used by the
clients with tokens or *credits*. The credits are an integer unit and
clients with tokens or *credits*. The credits are an float unit and
their monetary value depends on the service and is decided by the
provider. This could be:
@@ -157,7 +160,8 @@ The first step is to register your service on the IAP endpoint (production
and/or test) before you can actually query user accounts. To create a service,
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
production, https://iap-sandbox.odoo.com for testing, the endpoints are
*independent* and *not synchronized*).
*independent* and *not synchronized*). Alternatively, you can go to your portal
on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*.
.. note::
@@ -166,33 +170,45 @@ production, https://iap-sandbox.odoo.com for testing, the endpoints are
on sandbox to ease the tests.
Log in then go to :menuselection:`My Account --> Your In-App Services`, click
Create and provide the name of your service.
Create and provide the informations of your service.
The now created service has *two* important fields:
The service has *four* important fields:
* :samp:`name` - :class:`ServiceName`: this will identify your service in the
client's :ref:`app <iap-odoo-app>` communicates directly with IAP.
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
the client's account.
client's :ref:`app <iap-odoo-app>` communicates directly with IAP, choose it
carefully!
.. warning::
The :class:`ServiceName` is unique and should usually match the name of your
Odoo App.
Odoo Client App.
* :samp:`Icon` - :class:`Icon`: A generic icon that will serve as default for your
:ref:`packages <iap-packages>`
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
the client's account. It will be shown only once upon creation of the service
and can be regenerated at will.
.. danger::
Your :class:`ServiceKey` *is a secret*, leaking your service key
allows other application developers to draw credits bought for
your service(s).
.. image:: images/service_select.png
* :samp:`privacy policy` - :class:`PrivacyPolicy`: This is an url to the privacy
policy of your service. This should explicitly mention the **information you collect**,
How you **use it, its relevance** to make your service work and inform the
client on how they can **access, update or delete their personal information**.
.. image:: images/menu.png
:align: center
.. image:: images/service_create.png
.. image:: images/service_list.png
:align: center
.. image:: images/service_packs.png
.. image:: images/creating_service.png
:align: center
.. image:: images/service_created.png
:align: center
You can then create *credit packs* which clients can purchase in order to
@@ -203,9 +219,10 @@ use your service.
Packages
--------
The credit packages are essentially a product with 4 characteristics.
The credit packages are essentially a product with 5 characteristics.
* Name: the name of the package,
* Icon: A specific icon for the package. If not provided, it will fallback on the service icon
* Description: details on the package that will appear on the shop page as
well as the invoice,
* Credits: the amount of credits the client is entitled to when buying the package,
@@ -362,7 +379,6 @@ parameters we can use to make things clearer to the end-user:
.. patch::
.. TODO:: how do you test your service?
JSON-RPC2_ Transaction API
@@ -394,13 +410,13 @@ Authorize
:param ServiceKey key:
:param UserToken account_token:
:param int credit:
:param float credit:
:param str description: optional, helps users identify the reason for
charges on their accounts.
:returns: :class:`TransactionToken` if the authorization succeeded.
:raises: :class:`~odoo.exceptions.AccessError` if the service token is invalid
:raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does
:raises: ``TypeError`` if the ``credit`` value is not an integer
:raises: ``TypeError`` if the ``credit`` value is not an integer or a float
.. code-block:: python
@@ -434,7 +450,7 @@ Capture
:param TransactionToken token:
:param ServiceKey key:
:param int credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
:param float credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
:raises: :class:`~odoo.exceptions.AccessError`
.. code-block:: python
@@ -534,6 +550,33 @@ care how they are implemented
Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
Test the API
------------
In order to test the developped app, we propose a sandbox platform that allows you to:
1. Test the whole flow from the client's point of view - Actual services and transactions
that can be consulted. (again this requires to change the endpoint, see the danger note
in :ref:`Service <iap-service>`)
2. Test the API.
The latter consists in specific tokens that will work on **IAP-Sandbox only**.
* token ``000000``: represents a non-existing account. Returns
an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
* token ``000111``: Represents an account without sufficient credits to perform any service.
Returns an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
* token ``111111``: Represents an account with enough credits to perform any service.
An authorize attempt will return a dummy transacion token that is processed by the capture
and cancel routes.
.. note::
* Those tokens are only active on the IAP-Sanbox server.
* The service key is completely ignored with this flow, If you want to run a robust test
of your service, you should ignore these tokens.
Odoo Helpers
============
@@ -548,7 +591,7 @@ Charging
.. note::
A new functionality was introduced to capture a different amount of credits than reserved.
As this patch was added on the 15th of January 2018, you will need to upgrade your ``iap`` module in order to use it.
As this patch was added on the **15th of January 2018**, you will need to upgrade your ``iap`` module in order to use it.
The specifics of the new functionality are highlighted in the code.
.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template])
@@ -567,12 +610,12 @@ Charging
configuration key
:param ServiceKey key:
:param UserToken token:
:param int credit:
:param float credit:
:param str description:
:param Qweb template credit_template:
.. code-block:: python
:emphasize-lines: 10,13,14,15
:emphasize-lines: 11,13,14,15
@route('/deathstar/superlaser', type='json')
def superlaser(self, user_account,
@@ -583,7 +626,8 @@ Charging
0.0 is none, 1.0 is full power
"""
credits = int(MAXIMUM_POWER * factor)
with charge(request.env, SERVICE_KEY, user_account, credits) as transaction:
description = "We will demonstrate the power of this station on your home planet of Alderaan."
with charge(request.env, SERVICE_KEY, user_account, credits, description) as transaction:
# TODO: allow other targets
transaction.credit = max(credits, 2)
# Sales ongoing one the energy price,
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

+1 -1
View File
@@ -61,7 +61,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",
"base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1"
"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True","7"
"base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True","1"
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","1"
"base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S %p","True","1"
"base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1"
"base.lang_es_GT","Spanish (GT) / Español (GT)","es_GT","es_GT","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7"
"base.lang_es_MX","Spanish (MX) / Español (MX)","es_MX","es_MX","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7"
1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format translatable week_start
61 base.lang_es_CL Spanish (CL) / Español (CL) es_CL es_CL Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S True 1
62 base.lang_es_CO Spanish (CO) / Español (CO) es_CO es_CO Left-to-Right [3,0] , . %d-%m-%Y %H:%M:%S True 7
63 base.lang_es_CR Spanish (CR) / Español (CR) es_CR es_CR Left-to-Right [3,0] , %d/%m/%Y %H:%M:%S True 1
64 base.lang_es_DO Spanish (DO) / Español (DO) es_DO es_DO Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S %I:%M:%S %p True 1
65 base.lang_es_EC Spanish (EC) / Español (EC) es_EC es_EC Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S True 1
66 base.lang_es_GT Spanish (GT) / Español (GT) es_GT es_GT Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S True 7
67 base.lang_es_MX Spanish (MX) / Español (MX) es_MX es_MX Left-to-Right [3,0] . , %d/%m/%Y %H:%M:%S True 7
+2 -1
View File
@@ -670,8 +670,9 @@
<field name="symbol">RD$</field>
<field name="rounding">0.01</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Peso</field>
<field name="currency_unit_label">Pesos</field>
<field name="currency_subunit_label">Centavos</field>
<field name="position">before</field>
</record>
<record id="ECS" model="res.currency">
+3 -4
View File
@@ -695,11 +695,10 @@ class Module(models.Model):
mod.write(updated_values)
else:
mod_path = modules.get_module_path(mod_name)
if not mod_path:
if not mod_path or not terp:
continue
if not terp or not terp.get('installable', True):
continue
mod = self.create(dict(name=mod_name, state='uninstalled', **values))
state = "uninstalled" if terp.get('installable', True) else "uninstallable"
mod = self.create(dict(name=mod_name, state=state, **values))
res[1] += 1
mod._update_dependencies(terp.get('depends', []))
+10 -5
View File
@@ -387,8 +387,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
def copy(self, values):
raise UserError(_("Cannot duplicate configuration!"), "")
# TODO: Find replacement for 'onchange' attribute in view with dynamic
# api.onchange(...) and migrate the onchange_module(...) accordingly.
@api.model
def fields_view_get(self, view_id=None, view_type='form',
toolbar=False, submenu=False):
@@ -410,9 +408,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
modifiers = json.loads(node.get("modifiers"))
modifiers['readonly'] = True
node.set("modifiers", json.dumps(modifiers))
if 'on_change' not in node.attrib:
node.set("on_change",
"onchange_module(%s, '%s')" % (field, field))
ret_val['arch'] = etree.tostring(doc, encoding='unicode')
return ret_val
@@ -436,6 +431,16 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
}
return {}
def _register_hook(self):
""" Add an onchange method for each module field. """
def make_method(name):
return lambda self: self.onchange_module(self[name], name)
for name in self._fields:
if name.startswith('module_'):
method = make_method(name)
self._onchange_methods[name].append(method)
@api.model
def _get_classified_fields(self):
""" return a dictionary with the fields classified by category::
+1 -1
View File
@@ -2822,7 +2822,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
record._cache.update(record._convert_to_cache(values, validate=False))
if not self.env.cache.contains(self, field):
exc = AccessError("No value found for %s.%s" % (self, field.name))
self.env.cache.set_failed(self, field, exc)
self.env.cache.set_failed(self, [field], exc)
@api.multi
def _read_from_database(self, field_names, inherited_field_names=[]):
+2 -2
View File
@@ -56,8 +56,8 @@ class MigrationManager(object):
function. Theses files must respect a directory tree structure: A 'migrations' folder
which containt a folder by version. Version can be 'module' version or 'server.module'
version (in this case, the files will only be processed by this version of the server).
Python file names must start by `pre` or `post` and will be executed, respectively,
before and after the module initialisation. `end` scripts are run after all modules have
Python file names must start by `pre-` or `post-` and will be executed, respectively,
before and after the module initialisation. `end-` scripts are run after all modules have
been updated.
Example:
<moduledir>
+2 -1
View File
@@ -530,7 +530,8 @@ class ChromeBrowser():
'--user-data-dir': self.user_data_dir,
'--disable-translate': '',
'--window-size': '1366x768',
'--remote-debugging-port': str(self.devtools_port)
'--remote-debugging-port': str(self.devtools_port),
'--no-sandbox': '',
}
cmd = [self.executable]
cmd += ['%s=%s' % (k, v) if v else k for k, v in switches.items()]
+1 -1
View File
@@ -33,7 +33,7 @@ pytz==2016.7
pyusb==1.0.0
qrcode==5.3
reportlab==3.3.0
requests==2.11.1
requests==2.20.0
suds-jurko==0.6
vatnumber==1.2
vobject==0.9.3
+1 -1
View File
@@ -27,7 +27,7 @@ pytz==2016.7
pyusb==1.0.0
qrcode==5.3
reportlab>=3.3.0
requests==2.11.1
requests==2.20.0
six==1.10.0
suds-jurko==0.6
vatnumber==1.2