[MERGE] forward port branch saas-11.3 up to 1e0a92c996
This commit is contained in:
@@ -284,6 +284,12 @@ class AccountAccount(models.Model):
|
||||
self.filtered(lambda r: not r.reconcile)._toggle_reconcile_to_true()
|
||||
else:
|
||||
self.filtered(lambda r: r.reconcile)._toggle_reconcile_to_false()
|
||||
|
||||
if vals.get('currency_id'):
|
||||
for account in self:
|
||||
if self.env['account.move.line'].search_count([('account_id', '=', account.id), ('currency_id', 'not in', (False, vals['currency_id']))]):
|
||||
raise UserError(_('You cannot set a currency on this account as it already has some journal entries having a different foreign currency.'))
|
||||
|
||||
return super(AccountAccount, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
@@ -1086,6 +1092,7 @@ class AccountTax(models.Model):
|
||||
'refund_account_id': tax.refund_account_id.id,
|
||||
'analytic': tax.analytic,
|
||||
'price_include': tax.price_include,
|
||||
'tax_exigibility': tax.tax_exigibility,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1461,18 +1461,7 @@ class AccountInvoice(models.Model):
|
||||
new_invoices += refund_invoice
|
||||
return new_invoices
|
||||
|
||||
@api.multi
|
||||
def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None):
|
||||
""" Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice.
|
||||
|
||||
:param pay_journal: journal in which the payment entry will be created
|
||||
:param pay_amount: amount of the payment to register, defaults to the residual of the invoice
|
||||
:param date: payment date, defaults to fields.Date.context_today(self)
|
||||
:param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid
|
||||
"""
|
||||
if isinstance(pay_journal, pycompat.integer_types):
|
||||
pay_journal = self.env['account.journal'].browse([pay_journal])
|
||||
assert len(self) == 1, "Can only pay one invoice at a time."
|
||||
def _prepare_payment_vals(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None, communication=None):
|
||||
payment_type = self.type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound'
|
||||
if payment_type == 'inbound':
|
||||
payment_method = self.env.ref('account.account_payment_method_manual_in')
|
||||
@@ -1481,9 +1470,10 @@ class AccountInvoice(models.Model):
|
||||
payment_method = self.env.ref('account.account_payment_method_manual_out')
|
||||
journal_payment_methods = pay_journal.outbound_payment_method_ids
|
||||
|
||||
communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number
|
||||
if self.origin:
|
||||
communication = '%s (%s)' % (communication, self.origin)
|
||||
if not communication:
|
||||
communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number
|
||||
if self.origin:
|
||||
communication = '%s (%s)' % (communication, self.origin)
|
||||
|
||||
payment_vals = {
|
||||
'invoice_ids': [(6, 0, self.ids)],
|
||||
@@ -1498,7 +1488,22 @@ class AccountInvoice(models.Model):
|
||||
'payment_difference_handling': writeoff_acc and 'reconcile' or 'open',
|
||||
'writeoff_account_id': writeoff_acc and writeoff_acc.id or False,
|
||||
}
|
||||
return payment_vals
|
||||
|
||||
@api.multi
|
||||
def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None):
|
||||
""" Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice.
|
||||
|
||||
:param pay_journal: journal in which the payment entry will be created
|
||||
:param pay_amount: amount of the payment to register, defaults to the residual of the invoice
|
||||
:param date: payment date, defaults to fields.Date.context_today(self)
|
||||
:param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid
|
||||
"""
|
||||
if isinstance(pay_journal, pycompat.integer_types):
|
||||
pay_journal = self.env['account.journal'].browse([pay_journal])
|
||||
assert len(self) == 1, "Can only pay one invoice at a time."
|
||||
|
||||
payment_vals = self._prepare_payment_vals(pay_journal, pay_amount=pay_amount, date=date, writeoff_acc=writeoff_acc)
|
||||
payment = self.env['account.payment'].create(payment_vals)
|
||||
payment.post()
|
||||
|
||||
|
||||
@@ -779,7 +779,7 @@ var StatementModel = BasicModel.extend({
|
||||
}).then(function (result) {
|
||||
if (result.length > 0) {
|
||||
var line = self.getLine(handle);
|
||||
self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
|
||||
self.lines[handle].st_line.open_balance_account_id = line.balance.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -836,6 +836,7 @@ var StatementModel = BasicModel.extend({
|
||||
'__focus': false
|
||||
});
|
||||
|
||||
prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined
|
||||
prop.amount = tax.base;
|
||||
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
|
||||
prop.invalid = !self._isValid(prop);
|
||||
@@ -1129,6 +1130,7 @@ var StatementModel = BasicModel.extend({
|
||||
name : prop.label,
|
||||
debit : amount > 0 ? amount : 0,
|
||||
credit : amount < 0 ? -amount : 0,
|
||||
tax_exigible: prop.tax_exigible,
|
||||
analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]]
|
||||
};
|
||||
if (!isNaN(prop.id)) {
|
||||
@@ -1191,7 +1193,15 @@ var ManualModel = StatementModel.extend({
|
||||
self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code'));
|
||||
});
|
||||
|
||||
return def_account.then(function () {
|
||||
var def_reconcileModel = this._rpc({
|
||||
model: 'account.reconcile.model',
|
||||
method: 'search_read',
|
||||
})
|
||||
.then(function (reconcileModels) {
|
||||
self.reconcileModels = reconcileModels;
|
||||
});
|
||||
|
||||
return $.when(def_reconcileModel, def_account).then(function () {
|
||||
switch(context.mode) {
|
||||
case 'customers':
|
||||
case 'suppliers':
|
||||
@@ -1391,7 +1401,7 @@ var ManualModel = StatementModel.extend({
|
||||
offset: 0,
|
||||
limitMoveLines: this.limitMoveLines,
|
||||
filter: "",
|
||||
reconcileModels: [],
|
||||
reconcileModels: this.reconcileModels,
|
||||
account_id: this._formatNameGet([data.account_id, data.account_name]),
|
||||
st_line: data,
|
||||
visible: true
|
||||
|
||||
@@ -33,6 +33,11 @@ class Partner(models.Model):
|
||||
street_number2 = fields.Char('Door', compute='_split_street', help="Door Number",
|
||||
inverse='_set_street', store=True)
|
||||
|
||||
@api.model
|
||||
def _address_fields(self):
|
||||
"""Returns the list of address fields that are synced from the parent."""
|
||||
return super(Partner, self)._address_fields() + ['street_name', 'street_number', 'street_number2']
|
||||
|
||||
def get_street_fields(self):
|
||||
"""Returns the fields that can be used in a street format.
|
||||
Overwrite this function if you want to add your own fields."""
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def geo_find(addr):
|
||||
|
||||
def geo_find(addr, apikey=False):
|
||||
if not addr:
|
||||
return None
|
||||
url = 'https://maps.googleapis.com/maps/api/geocode/json'
|
||||
|
||||
if not apikey:
|
||||
raise UserError(_('''API key for GeoCoding (Places) required.\n
|
||||
Save this key in System Parameters with key: google.api_key_geocode, value: <your api key>
|
||||
Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information.
|
||||
'''))
|
||||
|
||||
url = "https://maps.googleapis.com/maps/api/geocode/json"
|
||||
try:
|
||||
result = requests.get(url, params={'sensor': 'false', 'address': addr}).json()
|
||||
result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json()
|
||||
except Exception as e:
|
||||
raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e)
|
||||
|
||||
if result['status'] != 'OK':
|
||||
if result.get('error_message'):
|
||||
_logger.error(result['error_message'])
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -45,22 +57,26 @@ class ResPartner(models.Model):
|
||||
partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5))
|
||||
date_localization = fields.Date(string='Geolocation Date')
|
||||
|
||||
@classmethod
|
||||
def _geo_localize(cls, apikey, street='', zip='', city='', state='', country=''):
|
||||
search = geo_query_address(street=street, zip=zip, city=city, state=state, country=country)
|
||||
result = geo_find(search, apikey)
|
||||
if result is None:
|
||||
search = geo_query_address(city=city, state=state, country=country)
|
||||
result = geo_find(search, apikey)
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def geo_localize(self):
|
||||
# We need country names in English below
|
||||
apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode')
|
||||
for partner in self.with_context(lang='en_US'):
|
||||
result = geo_find(geo_query_address(street=partner.street,
|
||||
zip=partner.zip,
|
||||
city=partner.city,
|
||||
state=partner.state_id.name,
|
||||
country=partner.country_id.name))
|
||||
if result is None:
|
||||
result = geo_find(geo_query_address(
|
||||
city=partner.city,
|
||||
state=partner.state_id.name,
|
||||
country=partner.country_id.name
|
||||
))
|
||||
|
||||
result = partner._geo_localize(apikey,
|
||||
partner.street,
|
||||
partner.zip,
|
||||
partner.city,
|
||||
partner.state_id.name,
|
||||
partner.country_id.name)
|
||||
if result:
|
||||
partner.write({
|
||||
'partner_latitude': result[0],
|
||||
|
||||
@@ -632,6 +632,8 @@ class Meeting(models.Model):
|
||||
recurring_date = recurring_date.replace(tzinfo=None)
|
||||
else:
|
||||
recurring_date = todate(meeting.recurrent_id_date)
|
||||
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]
|
||||
|
||||
@@ -85,14 +85,23 @@ class TestRecurrentEvent(common.TransactionCase):
|
||||
for meeting in meetings:
|
||||
self.assertEqual(meeting.name, 'Sprint Review for google modules', 'Name not changed for id: %s' % meeting.id)
|
||||
|
||||
# I detach first occurrence to check it is not modified by changing recurrent event.
|
||||
min(meetings, key=lambda m: m.start).detach_recurring_event()
|
||||
|
||||
# I change description of my weekly meeting Review code with programmer.
|
||||
idval = '%d-%s' % (self.calendar_event_sprint_review.id, '20110425124700')
|
||||
self.CalendarEvent.browse(idval).write({'description': 'Review code of the module: sync_google_calendar.'})
|
||||
|
||||
# I check whether that all the records of this recurrence has been edited.
|
||||
meetings = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)])
|
||||
for meeting in meetings:
|
||||
self.assertEqual(meeting.description, 'Review code of the module: sync_google_calendar.', 'Description not changed for id: %s' % meeting.id)
|
||||
# I check that detached event has not been edited.
|
||||
detached_meeting = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)])
|
||||
self.assertEqual(detached_meeting.description, False, 'Detached event description changed for id: %s' % meeting.id)
|
||||
|
||||
# I verify wether I find an event by date range when subsequent to a detached one.
|
||||
last_meeting = max(meetings, key=lambda m: m.start)
|
||||
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
|
||||
('start', '<=', last_meeting.stop), ('stop', '>=', last_meeting.start)
|
||||
])
|
||||
self.assertEqual(meetings.id, last_meeting.id, 'Last event should be found searching it by date range')
|
||||
|
||||
# I update the description of two meetings, and check that both have been updated
|
||||
self.calendar_event_sprint_review.write({'description': "Some description"})
|
||||
|
||||
@@ -938,7 +938,7 @@ class Lead(models.Model):
|
||||
email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
|
||||
email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
|
||||
sub_title = _('or send an email to %s') % (email_link)
|
||||
return '<p class="o_view_nocontent_smiling_face">%s</p><p>%s</p>' % (help_title, sub_title)
|
||||
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)
|
||||
|
||||
@api.multi
|
||||
def log_meeting(self, meeting_subject, meeting_date, duration):
|
||||
|
||||
@@ -147,14 +147,14 @@ class HrExpense(models.Model):
|
||||
|
||||
@api.model
|
||||
def get_empty_list_help(self, help_message):
|
||||
if help_message:
|
||||
if help_message and "oe_view_nocontent_smiling_face" not in help_message:
|
||||
use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
|
||||
alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
|
||||
if alias_record and alias_record.alias_domain and alias_record.alias_name:
|
||||
link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % {
|
||||
'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
|
||||
}
|
||||
return '<p class="oe_view_nocontent_smiling_face">%s</p><p>%s</p>' % (
|
||||
return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (
|
||||
_('Add a new expense,'),
|
||||
_('or send receipts by email to %s.') % (link),)
|
||||
return super(HrExpense, self).get_empty_list_help(help_message)
|
||||
|
||||
@@ -45,7 +45,7 @@ class ResPartner(models.Model):
|
||||
except ValueError:
|
||||
partner.l10n_co_verification_code = False
|
||||
|
||||
@api.constrains('vat', 'commercial_partner_country_id', 'l10n_co_document_type')
|
||||
@api.constrains('vat', 'country_id', 'l10n_co_document_type')
|
||||
def check_vat(self):
|
||||
# check_vat is implemented by base_vat which this localization
|
||||
# doesn't directly depend on. It is however automatically
|
||||
|
||||
@@ -48,7 +48,11 @@ class ServerActions(models.Model):
|
||||
# TDE CLEANME: when going to new api with server action, remove action
|
||||
if not action.template_id or not self._context.get('active_id'):
|
||||
return False
|
||||
action.template_id.send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
|
||||
# Clean context from default_type to avoid making attachment
|
||||
# with wrong values in subsequent operations
|
||||
cleaned_ctx = dict(self.env.context)
|
||||
cleaned_ctx.pop('default_type', None)
|
||||
action.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
|
||||
return False
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -365,7 +365,9 @@ class MailThread(models.AbstractModel):
|
||||
'email_link': email_link
|
||||
}
|
||||
}
|
||||
return "%(static_help)s<p>%(dyn_help)s</p>" % {
|
||||
# do not add alias two times if it was added previously
|
||||
if "oe_view_nocontent_alias" not in help:
|
||||
return "%(static_help)s<p class='oe_view_nocontent_alias'>%(dyn_help)s</p>" % {
|
||||
'static_help': help,
|
||||
'dyn_help': _("Create a new %(document)s by sending an email to %(email_link)s") % {
|
||||
'document': document_name,
|
||||
|
||||
@@ -187,6 +187,28 @@ var Activity = AbstractActivityField.extend({
|
||||
this.trigger_up('reload_mail_fields', fieldsToReload);
|
||||
},
|
||||
|
||||
/** Binds a focusout handler on a bootstrap popover
|
||||
* Useful to do some operations on the popover's HTML,
|
||||
* like keeping the user's input for the feedback
|
||||
* @param {JQuery} $popover_el: the element on which
|
||||
* the popover() method has been called
|
||||
*/
|
||||
_bindPopoverFocusout: function ($popover_el) {
|
||||
var self = this;
|
||||
// Retrieve the actual popover's HTML
|
||||
var $popover = $popover_el.data("bs.popover").tip();
|
||||
var activity_id = $popover_el.data('activity-id');
|
||||
$popover.off('focusout');
|
||||
$popover.focusout(function (e) {
|
||||
// outside click of popover hides the popover
|
||||
// e.relatedTarget is the element receiving the focus
|
||||
self.feedbackValue[activity_id] = $popover.find('#activity_feedback').val().trim();
|
||||
if(!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
|
||||
$popover_el.popover('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// handlers
|
||||
_onClickRedirect: function (ev) {
|
||||
var id = $(ev.target).data('oe-id');
|
||||
@@ -236,19 +258,23 @@ var Activity = AbstractActivityField.extend({
|
||||
})
|
||||
.then(this._reload.bind(this, {activity: true}));
|
||||
},
|
||||
|
||||
_onMarkActivityDone: function (event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
this.feedbackValue = this.feedbackValue || {};
|
||||
var $popover_el = $(event.currentTarget);
|
||||
var activity_id = $popover_el.data('activity-id');
|
||||
var previous_activity_type_id = $popover_el.data('previous-activity-type-id');
|
||||
if (!$popover_el.data('bs.popover')) {
|
||||
this.feedbackValue[activity_id] = "";
|
||||
$popover_el.popover({
|
||||
title : _t('Feedback'),
|
||||
html: 'true',
|
||||
trigger:'click',
|
||||
content : function() {
|
||||
var $popover = $(QWeb.render("mail.activity_feedback_form", {'previous_activity_type_id': previous_activity_type_id}));
|
||||
$popover.find('#activity_feedback').val(self.feedbackValue[activity_id]);
|
||||
$popover.on('click', '.o_activity_popover_done_next', function () {
|
||||
var feedback = _.escape($popover.find('#activity_feedback').val());
|
||||
var previous_activity_type_id = $popover_el.data('previous-activity-type-id');
|
||||
@@ -272,14 +298,7 @@ var Activity = AbstractActivityField.extend({
|
||||
}).on("shown.bs.popover", function () {
|
||||
var $popover = $(this).data("bs.popover").tip();
|
||||
$popover.find('#activity_feedback').focus();
|
||||
$popover.off('focusout');
|
||||
$popover.focusout(function (e) {
|
||||
// outside click of popover hide the popover
|
||||
// e.relatedTarget is the element receiving the focus
|
||||
if(!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
|
||||
$popover.popover('hide');
|
||||
}
|
||||
});
|
||||
self._bindPopoverFocusout($(this));
|
||||
}).popover('show');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,6 +17,8 @@ var createBusService = mailTestUtils.createBusService;
|
||||
var createAsyncView = testUtils.createAsyncView;
|
||||
var createView = testUtils.createView;
|
||||
|
||||
var Activity = require('mail.Activity');
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
|
||||
QUnit.module('Chatter', {
|
||||
@@ -72,6 +74,7 @@ QUnit.module('Chatter', {
|
||||
type: 'selection',
|
||||
selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
|
||||
},
|
||||
note : { string: "Note", type: "char" },
|
||||
},
|
||||
},
|
||||
'mail.activity.type': {
|
||||
@@ -143,6 +146,73 @@ QUnit.test('basic rendering', function (assert) {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Activity Done keep feedback on blur', function (assert) {
|
||||
assert.expect(3);
|
||||
var done = assert.async();
|
||||
|
||||
this.data['mail.activity'].records = [
|
||||
{activity_type_id: 1, id: 1, user_id: 2, state: 'today', note: 'But I\'m talkin\' about Shaft'},
|
||||
];
|
||||
this.data.partner.records[0].activity_ids = [1];
|
||||
|
||||
var shownDef = $.Deferred();
|
||||
var hiddenDef = $.Deferred();
|
||||
testUtils.patch(Activity, {
|
||||
_bindPopoverFocusout: function () {
|
||||
this._super.apply(this, arguments);
|
||||
shownDef.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
res_id: 2,
|
||||
arch:'<form string="Partners">' +
|
||||
'<div class="oe_chatter">' +
|
||||
'<field name="activity_ids" widget="mail_activity"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
// sanity checks
|
||||
var $activityEl = form.$('.o_mail_activity[name=activity_ids]');
|
||||
assert.strictEqual($activityEl.find('.o_thread_message').length, 1,
|
||||
'There should be one activity');
|
||||
assert.strictEqual($activityEl.find('.o_thread_message .o_thread_message_note').text().trim(),
|
||||
'But I\'m talkin\' about Shaft', 'The activity should have the right note');
|
||||
|
||||
var $popoverEl = $activityEl.find('.o_thread_message_tools .o_activity_done');
|
||||
$popoverEl.on('hidden.bs.popover', hiddenDef.resolve.bind(hiddenDef));
|
||||
|
||||
// open popover
|
||||
$popoverEl.click();
|
||||
|
||||
shownDef.then(function () {
|
||||
// write a feedback and focusout
|
||||
var $feedbackPopover = $popoverEl.data('bs.popover').tip();
|
||||
$feedbackPopover.find('#activity_feedback').val('John Shaft').focusout();
|
||||
|
||||
hiddenDef.then(function () {
|
||||
shownDef = $.Deferred();
|
||||
|
||||
// re-open popover
|
||||
$popoverEl.click();
|
||||
|
||||
shownDef.then(function () {
|
||||
var $feedbackPopover = $popoverEl.data('bs.popover').tip();
|
||||
assert.strictEqual($feedbackPopover.find('#activity_feedback').val(), 'John Shaft',
|
||||
"feedback should have been kept");
|
||||
|
||||
form.destroy();
|
||||
testUtils.unpatch(Activity);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('chatter in create mode', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
|
||||
@@ -164,8 +164,10 @@ class Partner(models.Model):
|
||||
|
||||
@api.model
|
||||
def _cron_update_membership(self):
|
||||
# used to recompute 'membership_state'; should no longer be necessary
|
||||
pass
|
||||
partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])])
|
||||
# mark the field to be recomputed, and recompute it
|
||||
partners._recompute_todo(self._fields['membership_state'])
|
||||
self.recompute()
|
||||
|
||||
@api.multi
|
||||
def create_membership_invoice(self, product_id=None, datas=None):
|
||||
|
||||
@@ -77,6 +77,8 @@ class ChangeProductionQty(models.TransientModel):
|
||||
wo.qty_producing = quantity
|
||||
if wo.qty_produced < wo.qty_production and wo.state == 'done':
|
||||
wo.state = 'progress'
|
||||
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
|
||||
wo.state = 'done'
|
||||
# assign moves; last operation receive all unassigned moves
|
||||
# TODO: following could be put in a function as it is similar as code in _workorders_create
|
||||
# TODO: only needed when creating new moves
|
||||
|
||||
@@ -196,9 +196,13 @@ class PaymentAcquirer(models.Model):
|
||||
def _check_required_if_provider(self):
|
||||
""" If the field has 'required_if_provider="<provider>"' attribute, then it
|
||||
required if record.provider is <provider>. """
|
||||
empty_field = []
|
||||
for acquirer in self:
|
||||
if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()):
|
||||
return False
|
||||
for k, f in acquirer._fields.items():
|
||||
if getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k]:
|
||||
empty_field.append(self.env['ir.model.fields'].search([('name', '=', k), ('model', '=', acquirer._name)]).field_description)
|
||||
if empty_field:
|
||||
raise ValidationError((', ').join(empty_field))
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
Operation type used to record product pickings
|
||||
</div>
|
||||
<div class="content-group mt16">
|
||||
<field name="picking_type_id"/>
|
||||
<field name="picking_type_id" required="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
on <field name="create_date" />
|
||||
</li>
|
||||
</ul>
|
||||
<i t-if="record.feedback.raw_value" class="fa fa-comment pull-right mt4" t-attf-title="Feedback:\n#{record.feedback.raw_value}" />
|
||||
<i t-if="record.feedback.raw_value" class="fa fa-comment pull-right mt4" t-att-title="record.feedback.raw_value" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -384,7 +384,7 @@
|
||||
<field name="team_id" options="{'no_create': True}"/>
|
||||
<field name="client_order_ref"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
<field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting" force_save="1"/>
|
||||
</group>
|
||||
<group name="sale_pay" string="Invoicing">
|
||||
<field name="date_order" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}"/>
|
||||
|
||||
@@ -294,6 +294,13 @@ class SaleOrderLine(models.Model):
|
||||
})
|
||||
return values
|
||||
|
||||
def _get_qty_procurement(self):
|
||||
self.ensure_one()
|
||||
qty = 0.0
|
||||
for move in self.move_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
return qty
|
||||
|
||||
@api.multi
|
||||
def _action_launch_procurement_rule(self):
|
||||
"""
|
||||
@@ -306,9 +313,7 @@ class SaleOrderLine(models.Model):
|
||||
for line in self:
|
||||
if line.state != 'sale' or not line.product_id.type in ('consu','product'):
|
||||
continue
|
||||
qty = 0.0
|
||||
for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
|
||||
qty = line._get_qty_procurement()
|
||||
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
|
||||
continue
|
||||
|
||||
|
||||
@@ -51,15 +51,19 @@ class AccountInvoice(models.Model):
|
||||
self._compute_timesheet_revenue()
|
||||
return result
|
||||
|
||||
def _get_compute_timesheet_revenue_domain(self, so_line_ids):
|
||||
return [
|
||||
('so_line', 'in', so_line_ids),
|
||||
('project_id', '!=', False),
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed'])
|
||||
]
|
||||
|
||||
def _compute_timesheet_revenue(self):
|
||||
for invoice in self:
|
||||
for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)):
|
||||
uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([
|
||||
('so_line', 'in', invoice_line.sale_line_ids.ids),
|
||||
('project_id', '!=', False),
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed'])
|
||||
])
|
||||
domain = self._get_compute_timesheet_revenue_domain(invoice_line.sale_line_ids.ids)
|
||||
uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search(domain)
|
||||
|
||||
# NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs)
|
||||
if uninvoiced_timesheet_lines:
|
||||
|
||||
@@ -457,8 +457,9 @@ class Picking(models.Model):
|
||||
else:
|
||||
location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations()
|
||||
|
||||
self.location_id = location_id
|
||||
self.location_dest_id = location_dest_id
|
||||
if self.state == 'draft':
|
||||
self.location_id = location_id
|
||||
self.location_dest_id = location_dest_id
|
||||
# TDE CLEANME move into onchange_partner_id
|
||||
if self.partner_id and self.partner_id.picking_warn:
|
||||
if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
|
||||
|
||||
@@ -24,23 +24,24 @@ class TestPackingNeg(TransactionCase):
|
||||
})
|
||||
|
||||
# Create an incoming picking for this product of 300 PCE from suppliers to stock
|
||||
default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
|
||||
default_get_vals.update({
|
||||
vals = {
|
||||
'name': 'Incoming picking (negative product)',
|
||||
'partner_id': self.ref('base.res_partner_2'),
|
||||
'picking_type_id': self.ref('stock.picking_type_in'),
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': 'NEG',
|
||||
'product_id': product_neg.id,
|
||||
'product_uom': product_neg.uom_id.id,
|
||||
'product_uom_qty': 300.00,
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
})],
|
||||
})
|
||||
pick_neg = self.env['stock.picking'].new(default_get_vals)
|
||||
}
|
||||
pick_neg = self.env['stock.picking'].create(vals)
|
||||
pick_neg.onchange_picking_type()
|
||||
pick_neg.move_lines.onchange_product_id()
|
||||
vals = pick_neg._convert_to_write(pick_neg._cache)
|
||||
pick_neg = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Confirm and assign picking
|
||||
pick_neg.action_confirm()
|
||||
@@ -80,23 +81,24 @@ class TestPackingNeg(TransactionCase):
|
||||
pick_neg.action_done()
|
||||
|
||||
# Make a delivery order of 300 pieces to the customer
|
||||
default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
|
||||
default_get_vals.update({
|
||||
vals = {
|
||||
'name': 'outgoing picking (negative product)',
|
||||
'partner_id': self.ref('base.res_partner_4'),
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': 'NEG',
|
||||
'product_id': product_neg.id,
|
||||
'product_uom': product_neg.uom_id.id,
|
||||
'product_uom_qty': 300.00,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})],
|
||||
})
|
||||
delivery_order_neg = self.env['stock.picking'].new(default_get_vals)
|
||||
}
|
||||
delivery_order_neg = self.env['stock.picking'].create(vals)
|
||||
delivery_order_neg.onchange_picking_type()
|
||||
delivery_order_neg.move_lines.onchange_product_id()
|
||||
vals = delivery_order_neg._convert_to_write(delivery_order_neg._cache)
|
||||
delivery_order_neg = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Assign and confirm
|
||||
delivery_order_neg.action_confirm()
|
||||
@@ -136,23 +138,24 @@ class TestPackingNeg(TransactionCase):
|
||||
self.assertEqual(pallet_3_stock_qty, 50, "Should have 50 pieces in stock on pallet 3")
|
||||
|
||||
# Create a picking for reconciling the negative quant
|
||||
default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
|
||||
default_get_vals.update({
|
||||
vals = {
|
||||
'name': 'reconciling_delivery',
|
||||
'partner_id': self.ref('base.res_partner_4'),
|
||||
'picking_type_id': self.ref('stock.picking_type_in'),
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': 'NEG',
|
||||
'product_id': product_neg.id,
|
||||
'product_uom': product_neg.uom_id.id,
|
||||
'product_uom_qty': 20.0,
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
})],
|
||||
})
|
||||
delivery_reconcile = self.env['stock.picking'].new(default_get_vals)
|
||||
}
|
||||
delivery_reconcile = self.env['stock.picking'].create(vals)
|
||||
delivery_reconcile.onchange_picking_type()
|
||||
delivery_reconcile.move_lines.onchange_product_id()
|
||||
vals = delivery_reconcile._convert_to_write(delivery_reconcile._cache)
|
||||
delivery_reconcile = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Receive 20 products with lot neg in stock with a new incoming shipment that should be on pallet 2
|
||||
delivery_reconcile.action_confirm()
|
||||
|
||||
@@ -27,26 +27,23 @@ class TestProcRule(TransactionCase):
|
||||
'route_ids': [(4, product_route.id)]})
|
||||
|
||||
# Create Delivery Order of 10 `product.product_product_3` from Output -> Customer
|
||||
default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
|
||||
default_get_vals.update({
|
||||
product = self.env.ref('product.product_product_3')
|
||||
vals = {
|
||||
'name': 'Delivery order for procurement',
|
||||
'partner_id': self.ref('base.res_partner_2'),
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
'location_id': self.ref('stock.stock_location_output'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_lines': [(0, 0, {
|
||||
'product_id': self.ref('product.product_product_3'),
|
||||
'name': '/',
|
||||
'product_id': product.id,
|
||||
'product_uom': product.uom_id.id,
|
||||
'product_uom_qty': 10.00,
|
||||
'procure_method': 'make_to_order',
|
||||
})],
|
||||
})
|
||||
pick_output = self.env['stock.picking'].new(default_get_vals)
|
||||
pick_output.onchange_picking_type()
|
||||
pick_output.move_lines.onchange_product_id()
|
||||
pick_output.update({
|
||||
'location_id': self.ref('stock.stock_location_output'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})
|
||||
vals = pick_output._convert_to_write(pick_output._cache)
|
||||
}
|
||||
pick_output = self.env['stock.picking'].create(vals)
|
||||
pick_output.move_lines.onchange_product_id()
|
||||
|
||||
# Confirm delivery order.
|
||||
pick_output.action_confirm()
|
||||
|
||||
@@ -17,25 +17,25 @@ class TestWiseOperator(TransactionCase):
|
||||
'uom_po_id': self.ref('uom.product_uom_unit'),
|
||||
})
|
||||
|
||||
picking_default_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
|
||||
|
||||
# Create an incoming picking for this product of 10 PCE from suppliers to stock
|
||||
vals = dict(picking_default_vals, **{
|
||||
vals = {
|
||||
'name': 'Incoming picking (wise unit)',
|
||||
'partner_id': self.ref('base.res_partner_2'),
|
||||
'picking_type_id': self.ref('stock.picking_type_in'),
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': '/',
|
||||
'product_id': product_wise.id,
|
||||
'product_uom': product_wise.uom_id.id,
|
||||
'product_uom_qty': 10.00,
|
||||
'location_id': self.ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': self.ref('stock.stock_location_stock'),
|
||||
})],
|
||||
})
|
||||
pick1_wise = self.env['stock.picking'].new(vals)
|
||||
}
|
||||
pick1_wise = self.env['stock.picking'].create(vals)
|
||||
pick1_wise.onchange_picking_type()
|
||||
pick1_wise.move_lines.onchange_product_id()
|
||||
vals = pick1_wise._convert_to_write(pick1_wise._cache)
|
||||
pick1_wise = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Confirm and assign picking and prepare partial
|
||||
pick1_wise.action_confirm()
|
||||
@@ -65,22 +65,24 @@ class TestWiseOperator(TransactionCase):
|
||||
self.assertEqual(len(records.ids), 3, "The number of quants created is not correct")
|
||||
|
||||
# Make a delivery order of 5 pieces to the customer
|
||||
vals = dict(picking_default_vals, **{
|
||||
vals = {
|
||||
'name': 'outgoing picking 1 (wise unit)',
|
||||
'partner_id': self.ref('base.res_partner_4'),
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': '/',
|
||||
'product_id': product_wise.id,
|
||||
'product_uom': product_wise.uom_id.id,
|
||||
'product_uom_qty': 5.0,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})],
|
||||
})
|
||||
delivery_order_wise1 = self.env['stock.picking'].new(vals)
|
||||
}
|
||||
delivery_order_wise1 = self.env['stock.picking'].create(vals)
|
||||
delivery_order_wise1.onchange_picking_type()
|
||||
delivery_order_wise1.move_lines.onchange_product_id()
|
||||
vals = delivery_order_wise1._convert_to_write(delivery_order_wise1._cache)
|
||||
delivery_order_wise1 = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Assign and confirm
|
||||
delivery_order_wise1.action_confirm()
|
||||
@@ -88,22 +90,24 @@ class TestWiseOperator(TransactionCase):
|
||||
self.assertEqual(delivery_order_wise1.state, 'assigned')
|
||||
|
||||
# Make a delivery order of 5 pieces to the customer
|
||||
vals = dict(picking_default_vals, **{
|
||||
vals = {
|
||||
'name': 'outgoing picking 2 (wise unit)',
|
||||
'partner_id': self.ref('base.res_partner_4'),
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
'move_lines': [(0, 0, {
|
||||
'name': '/',
|
||||
'product_id': product_wise.id,
|
||||
'product_uom': product_wise.uom_id.id,
|
||||
'product_uom_qty': 5.0,
|
||||
'location_id': self.ref('stock.stock_location_stock'),
|
||||
'location_dest_id': self.ref('stock.stock_location_customers'),
|
||||
})],
|
||||
})
|
||||
delivery_order_wise2 = self.env['stock.picking'].new(vals)
|
||||
}
|
||||
delivery_order_wise2 = self.env['stock.picking'].create(vals)
|
||||
delivery_order_wise2.onchange_picking_type()
|
||||
delivery_order_wise2.move_lines.onchange_product_id()
|
||||
vals = delivery_order_wise2._convert_to_write(delivery_order_wise2._cache)
|
||||
delivery_order_wise2 = self.env['stock.picking'].create(vals)
|
||||
|
||||
# Assign and confirm
|
||||
delivery_order_wise2.action_confirm()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import purchase
|
||||
from . import purchase
|
||||
from . import sale
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id')
|
||||
|
||||
@api.multi
|
||||
def _get_qty_procurement(self):
|
||||
if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
qty = 0.0
|
||||
for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
return qty
|
||||
else:
|
||||
return super(SaleOrderLine, self)._get_qty_procurement()
|
||||
|
||||
@@ -6,4 +6,3 @@ from . import test_dropship
|
||||
from . import test_lifo_price
|
||||
from . import test_procurement_exception
|
||||
from . import test_stockvaluation
|
||||
|
||||
|
||||
@@ -5,6 +5,50 @@ from odoo.tests import common, Form
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestDropship(common.TransactionCase):
|
||||
def test_change_qty(self):
|
||||
# enable the dropship and MTO route on the product
|
||||
prod = self.env.ref('product.product_product_8')
|
||||
dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping')
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
prod.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]})
|
||||
|
||||
# add a vendor
|
||||
vendor1 = self.env['res.partner'].create({'name': 'vendor1'})
|
||||
seller1 = self.env['product.supplierinfo'].create({
|
||||
'name': vendor1.id,
|
||||
'price': 8,
|
||||
})
|
||||
prod.write({'seller_ids': [(6, 0, [seller1.id])]})
|
||||
|
||||
# sell one unit of this product
|
||||
cust = self.env['res.partner'].create({'name': 'customer1'})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': cust.id,
|
||||
'partner_invoice_id': cust.id,
|
||||
'partner_shipping_id': cust.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': prod.name,
|
||||
'product_id': prod.id,
|
||||
'product_uom_qty': 1.00,
|
||||
'product_uom': prod.uom_id.id,
|
||||
'price_unit': 12,
|
||||
})],
|
||||
'pricelist_id': self.env.ref('product.list0').id,
|
||||
'picking_policy': 'direct',
|
||||
})
|
||||
so.action_confirm()
|
||||
po = self.env['purchase.order'].search([('group_id', '=', so.procurement_group_id.id)])
|
||||
po_line = po.order_line
|
||||
|
||||
# Check the qty on the P0
|
||||
self.assertAlmostEqual(po_line.product_qty, 1.00)
|
||||
|
||||
# Update qty on SO and check PO
|
||||
so.order_line.product_uom_qty = 2.00
|
||||
self.assertAlmostEqual(po_line.product_qty, 2.00)
|
||||
|
||||
|
||||
class TestDropship(common.TransactionCase):
|
||||
|
||||
def test_00_dropship(self):
|
||||
|
||||
@@ -182,7 +182,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
// If it is not handled, we should display something clearer than the common crash_manager error dialog
|
||||
// since it won't show anything except "Script error."
|
||||
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
|
||||
if (message === "Script error." && !file && !line && !col && !error) {
|
||||
if (!file && !line && !col) {
|
||||
// Chrome and Opera set "Script error." on the `message` and hide the `error`
|
||||
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
|
||||
if (window.onOriginError) {
|
||||
window.onOriginError();
|
||||
delete window.onOriginError;
|
||||
@@ -190,7 +192,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
crash_manager.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: _t("Unknown CORS error"),
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin.")},
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -61,15 +61,11 @@ var KanbanModel = BasicModel.extend({
|
||||
if (!groupByField || groupByField.type !== 'many2one') {
|
||||
return $.Deferred().reject(); // only supported when grouped on m2o
|
||||
}
|
||||
// ignore action default which may be for another model
|
||||
var context = _.pick(parent.context, function (value, key) {
|
||||
return key.slice(0,8) !== 'default_';
|
||||
});
|
||||
return this._rpc({
|
||||
model: groupByField.relation,
|
||||
method: 'name_create',
|
||||
args: [name],
|
||||
context: context, // todo: combine with view context
|
||||
context: parent.context, // todo: combine with view context
|
||||
})
|
||||
.then(function (result) {
|
||||
var newGroup = self._makeDataPoint({
|
||||
|
||||
@@ -193,6 +193,7 @@ var ListRenderer = BasicRenderer.extend({
|
||||
reject = columnInvisibleFields[c.attrs.name];
|
||||
}
|
||||
if (!reject && c.attrs.widget === 'handle') {
|
||||
self.hasHandle = true;
|
||||
self.handleField = c.attrs.name;
|
||||
}
|
||||
return reject;
|
||||
@@ -656,8 +657,6 @@ var ListRenderer = BasicRenderer.extend({
|
||||
this._computeAggregates();
|
||||
$table.toggleClass('o_list_view_grouped', is_grouped);
|
||||
$table.toggleClass('o_list_view_ungrouped', !is_grouped);
|
||||
this.hasHandle = this.state.orderedBy.length === 0 ||
|
||||
this.state.orderedBy[0].name === this.handleField;
|
||||
if (is_grouped) {
|
||||
$table
|
||||
.append(this._renderHeader(true))
|
||||
|
||||
@@ -1907,7 +1907,7 @@ QUnit.module('ActionManager', {
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.iframe.src = 'test: ' + self.iframe.getAttribute('src');
|
||||
self.iframe.src = 'test ' + self.iframe.getAttribute('src');
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1926,7 +1926,7 @@ QUnit.module('ActionManager', {
|
||||
if (route === '/report/check_wkhtmltopdf') {
|
||||
return $.when('broken');
|
||||
}
|
||||
if (route === 'test: /report/html/some_report') {
|
||||
if (route === 'test /report/html/some_report') {
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@@ -1947,7 +1947,7 @@ QUnit.module('ActionManager', {
|
||||
'/web/action/load',
|
||||
'/report/check_wkhtmltopdf',
|
||||
'notification',
|
||||
'test: /report/html/some_report', // report client action's iframe
|
||||
'test /report/html/some_report', // report client action's iframe
|
||||
]);
|
||||
|
||||
actionManager.destroy();
|
||||
|
||||
@@ -1157,43 +1157,6 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('m2o group record created does not use action defaults', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
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="foo"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
assert.notOk(_.has(args.kwargs.context, 'default_name'),
|
||||
"default_* should be removed from context");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
viewOptions: {
|
||||
context: {
|
||||
default_name: 'default name partner',
|
||||
},
|
||||
},
|
||||
});
|
||||
kanban.renderButtons();
|
||||
|
||||
kanban.$('.o_column_quick_create input').val('new product');
|
||||
kanban.$('.o_column_quick_create button.o_kanban_add').click();
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('quick create record fail in grouped', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
|
||||
@@ -3235,76 +3235,6 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
// we need another sortable field to lock/unlock the handle
|
||||
this.data.foo.fields.amount.sortable = true;
|
||||
// resequence makes sense on a sequence field, not on arbitrary fields
|
||||
this.data.foo.records[0].int_field = 0;
|
||||
this.data.foo.records[1].int_field = 1;
|
||||
this.data.foo.records[2].int_field = 2;
|
||||
this.data.foo.records[3].int_field = 3;
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top" default_order="int_field">' +
|
||||
'<field name="int_field" widget="handle"/>' +
|
||||
'<field name="amount" widget="float"/>' +
|
||||
'</tree>',
|
||||
});
|
||||
|
||||
assert.strictEqual(list.$('tbody').text(), '1200.00500.00300.000.00',
|
||||
"default should be sorted by int_field");
|
||||
|
||||
// Drag and drop the fourth line in second position
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(3),
|
||||
list.$('tbody tr').first(),
|
||||
{position: 'bottom'}
|
||||
);
|
||||
|
||||
// Handle should be unlocked at this point
|
||||
assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00',
|
||||
"drag and drop should have succeeded, as the handle is unlocked");
|
||||
|
||||
// Sorting by a field different for int_field should lock the handle
|
||||
list.$('.o_column_sortable').eq(1).click();
|
||||
|
||||
assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00',
|
||||
"should have been sorted by amount");
|
||||
|
||||
// Drag and drop the fourth line in second position (not)
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(3),
|
||||
list.$('tbody tr').first(),
|
||||
{position: 'bottom'}
|
||||
);
|
||||
|
||||
assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00',
|
||||
"drag and drop should have failed as the handle is locked");
|
||||
|
||||
// Sorting by int_field should unlock the handle
|
||||
list.$('.o_column_sortable').eq(0).click();
|
||||
|
||||
assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00',
|
||||
"records should be ordered as per the previous resequence");
|
||||
|
||||
// Drag and drop the fourth line in second position
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(3),
|
||||
list.$('tbody tr').first(),
|
||||
{position: 'bottom'}
|
||||
);
|
||||
|
||||
assert.strictEqual(list.$('tbody').text(), '1200.00300.000.00500.00',
|
||||
"drag and drop should have worked as the handle is unlocked");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list with handle widget with slow network', function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web_settings_dashboard', function (require) {
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var framework = require('web.framework');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
@@ -11,12 +11,12 @@ var Dashboard = webSettingsDashboard.Dashboard;
|
||||
function createDashboard(params) {
|
||||
var widget = new Widget();
|
||||
var dashboard = new Dashboard(widget);
|
||||
dashboard.all_dashboards = ['invitations']; // test only user invitations
|
||||
dashboard.all_dashboards = params.dashboards || ['invitations']; // test only user invitations
|
||||
|
||||
testUtils.addMockEnvironment(widget, params);
|
||||
|
||||
var originalDestroy = Dashboard.prototype.destroy;
|
||||
Dashboard.destroy = function () {
|
||||
dashboard.destroy = function () {
|
||||
dashboard.destroy = originalDestroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
@@ -86,19 +86,10 @@ class CrmLead(models.Model):
|
||||
if lead.partner_latitude and lead.partner_longitude:
|
||||
continue
|
||||
if lead.country_id:
|
||||
result = geo_find(geo_query_address(street=lead.street,
|
||||
zip=lead.zip,
|
||||
city=lead.city,
|
||||
state=lead.state_id.name,
|
||||
country=lead.country_id.name))
|
||||
|
||||
if result is None:
|
||||
result = geo_find(geo_query_address(
|
||||
city=lead.city,
|
||||
state=lead.state_id.name,
|
||||
country=lead.country_id.name
|
||||
))
|
||||
|
||||
apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode')
|
||||
result = self.env['res.partner']._geo_localize(apikey,
|
||||
lead.street, lead.zip, lead.city,
|
||||
lead.state_id.name, lead.country_id.name)
|
||||
if result:
|
||||
lead.write({
|
||||
'partner_latitude': result[0],
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestPartnerAssign(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestPartnerAssign, self).setUp()
|
||||
|
||||
def geo_find(addr):
|
||||
def geo_find(addr, apikey):
|
||||
return {
|
||||
'Wavre, Belgium': (50.7158956, 4.6128075),
|
||||
'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<template id="report_saleorder_validity_date" inherit_id="sale.report_saleorder_document">
|
||||
<xpath expr="//div[hasclass('page')]/p[@id='fiscal_position_remark']" position="after">
|
||||
<div t-if="doc.options and doc.state in ['draft', 'sent']" style="page-break-before:always;">
|
||||
<t t-set="has_option_discount" t-value="any(doc.options.filtered(lambda o: o.discount != 0.0))" />
|
||||
<h2>
|
||||
<span>Suggested Products</span>
|
||||
</h2>
|
||||
@@ -11,7 +12,7 @@
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Description</th>
|
||||
<th groups="sale.group_discount_per_so_line"></th>
|
||||
<th t-if="has_option_discount" groups="sale.group_discount_per_so_line"></th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -23,8 +24,8 @@
|
||||
<td>
|
||||
<span t-field="option.name"/>
|
||||
</td>
|
||||
<td groups="sale.group_discount_per_so_line">
|
||||
<strong class="text-info">
|
||||
<td t-if="has_option_discount" groups="sale.group_discount_per_so_line">
|
||||
<strong t-if="option.discount != 0.0" class="text-info">
|
||||
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
|
||||
</strong>
|
||||
</td>
|
||||
|
||||
@@ -105,7 +105,6 @@ class SaleOrder(models.Model):
|
||||
'order_id': order_id,
|
||||
'product_uom': product.uom_id.id,
|
||||
'price_unit': pu,
|
||||
'customer_lead': product.sale_delay,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -31,6 +31,13 @@ class SaleOrder(models.Model):
|
||||
values['warning'] = self.warning_stock
|
||||
return values
|
||||
|
||||
@api.multi
|
||||
def _website_product_id_change(self, order_id, product_id, qty=0):
|
||||
res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty)
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
res['customer_lead'] = product.sale_delay
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _get_stock_warning(self, clear=True):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -33,3 +33,4 @@ Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath
|
||||
Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s
|
||||
Simone Orsi simahawk@gmail.com https://github.com/simahawk
|
||||
Patrick Tombez patrick.tombez@camptocamp.com https://github.com/p-tombez
|
||||
Julien Coux julien.coux@camptocamp.com https://github.com/jcoux
|
||||
|
||||
@@ -21,3 +21,4 @@ Peter Hahn peter.hahn@initos.com https://github.com/codingforfun
|
||||
Claudia Haida claudia.haida@initos.com
|
||||
Andreas Zöllner andreas.zoellner@initos.com https://github.com/azoellner
|
||||
Rami Alwafaie rami.alwafaie@initos.com https://github.com/rami-wafaie
|
||||
Florian Kantelberg florian.kantelberg@initos.com https://github.com/fkantelberg
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Argentina, 2018-09-04
|
||||
|
||||
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,
|
||||
|
||||
Ivan Todorovich <ivan.todorovich@gmail.com> https://github.com/ivantodorovich
|
||||
@@ -0,0 +1,11 @@
|
||||
Guatemala, 2018-09-04
|
||||
|
||||
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,
|
||||
|
||||
Rodrigo Fernandez rodrigo@solucionesprisma.com https://github.com/rocketgithub
|
||||
@@ -823,6 +823,18 @@ element is ``<pivot>`` which can take the following attributes:
|
||||
|
||||
The elements allowed within a pivot view are the same as for the graph view.
|
||||
|
||||
In Pivot view a ``field`` can have a ``widget`` attribute to dictate its format.
|
||||
The widget should be a field formatter, of which the most interesting are
|
||||
``date``, ``datetime``, ``float_time``, and ``monetary``.
|
||||
|
||||
For instance a timesheet pivot view could be defined as::
|
||||
|
||||
<pivot string="Timesheet">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="date" interval="month" type="col"/>
|
||||
<field name="unit_amount" type="measure" widget="float_time"/>
|
||||
</pivot>
|
||||
|
||||
.. _reference/views/kanban:
|
||||
|
||||
Kanban
|
||||
|
||||
@@ -651,7 +651,7 @@
|
||||
<field name="name">Guatemala</field>
|
||||
<field name="code">gt</field>
|
||||
<field file="base/static/img/country_flags/gt.png" name="image" type="base64" />
|
||||
<field name="currency_id" ref="QTQ" />
|
||||
<field name="currency_id" ref="GTQ" />
|
||||
<field eval="502" name="phone_code" />
|
||||
</record>
|
||||
<record id="gu" model="res.country">
|
||||
|
||||
@@ -453,7 +453,7 @@ class Partner(models.Model):
|
||||
""" Sync commercial fields and address fields from company and to children after create/update,
|
||||
just as if those were all modeled as fields.related to the parent """
|
||||
# 1. From UPSTREAM: sync from parent
|
||||
if values.get('parent_id') or values.get('type', 'contact'):
|
||||
if values.get('parent_id') or values.get('type') == 'contact':
|
||||
# 1a. Commercial fields: sync if parent changed
|
||||
if values.get('parent_id'):
|
||||
self._commercial_sync_from_company()
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
#from . import models
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
#
|
||||
# Conditional installation of enterprise modules.
|
||||
#
|
||||
# This module is defined in community but some steps (defined with 'edition: "enterprise"')
|
||||
# are only used to test enterprise. As it's not possible to direcly add enterprise
|
||||
# modules dependencies, this post install hook will install account_accountant if exists.
|
||||
#
|
||||
def _auto_install_enterprise_dependencies(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
module_list = ['account_accountant']
|
||||
module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
|
||||
module_ids.sudo().button_install()
|
||||
|
||||
@@ -13,4 +13,5 @@ It will install some main apps and will try to execute the most important action
|
||||
'views/templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'post_init_hook': '_auto_install_enterprise_dependencies',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user