-
+
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index bafdf1cb464..581274ac404 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -31,10 +31,10 @@ class FetchmailServer(models.Model):
l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.")
l10n_it_last_uid = fields.Integer(string='Last message UID', default=1)
- @api.constrains('l10n_it_is_pec', 'type')
+ @api.constrains('l10n_it_is_pec', 'server_type')
def _check_pec(self):
for record in self:
- if record.l10n_it_is_pec and record.type != 'imap':
+ if record.l10n_it_is_pec and record.server_type != 'imap':
raise ValidationError(_("PEC mail server must be of type IMAP."))
@api.multi
@@ -43,7 +43,7 @@ class FetchmailServer(models.Model):
MailThread = self.env['mail.thread']
for server in self.filtered(lambda s: s.l10n_it_is_pec):
- _logger.info('start checking for new emails on %s PEC server %s', server.type, server.name)
+ _logger.info('start checking for new emails on %s PEC server %s', server.server_type, server.name)
count, failed = 0, 0
imap_server = None
@@ -81,14 +81,14 @@ class FetchmailServer(models.Model):
self._attachment_invoice(msg_txt)
new_max_uid = max(new_max_uid, int(uid))
except Exception:
- _logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True)
+ _logger.info('Failed to process mail from %s server %s.', server.server_type, server.name, exc_info=True)
failed += 1
self._cr.commit()
count += 1
server.write({'l10n_it_last_uid': new_max_uid})
- _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed)
+ _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.server_type, server.name, (count - failed), failed)
except Exception:
- _logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True)
+ _logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type, server.name, exc_info=True)
finally:
if imap_server:
imap_server.close()
diff --git a/addons/l10n_lu/__init__.py b/addons/l10n_lu/__init__.py
index 1947891c8f5..79508eb4f80 100644
--- a/addons/l10n_lu/__init__.py
+++ b/addons/l10n_lu/__init__.py
@@ -1,8 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, SUPERUSER_ID
+
from . import models
+def _post_init_hook(cr, registry):
+ _preserve_tag_on_taxes(cr, registry)
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ env.ref('l10n_lu.lu_2011_chart_1').process_coa_translations()
+
def _preserve_tag_on_taxes(cr, registry):
from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes
preserve_existing_tags_on_taxes(cr, registry, 'l10n_lu')
diff --git a/addons/l10n_lu/__manifest__.py b/addons/l10n_lu/__manifest__.py
index c5457fd82d4..9eabf73f08d 100644
--- a/addons/l10n_lu/__manifest__.py
+++ b/addons/l10n_lu/__manifest__.py
@@ -43,5 +43,5 @@ Notes:
'data/account.chart.template.csv',
'data/account_chart_template_data.xml',
],
- 'post_init_hook': '_preserve_tag_on_taxes',
+ 'post_init_hook': '_post_init_hook',
}
diff --git a/addons/l10n_ma/data/account_tax_data.xml b/addons/l10n_ma/data/account_tax_data.xml
index c112839f15b..cb134570086 100644
--- a/addons/l10n_ma/data/account_tax_data.xml
+++ b/addons/l10n_ma/data/account_tax_data.xml
@@ -2,10 +2,10 @@
-
-
+
+
-
+
diff --git a/addons/l10n_ma/data/l10n_ma_chart_data.xml b/addons/l10n_ma/data/l10n_ma_chart_data.xml
index d72021c2ae4..cec91faca6f 100644
--- a/addons/l10n_ma/data/l10n_ma_chart_data.xml
+++ b/addons/l10n_ma/data/l10n_ma_chart_data.xml
@@ -1190,13 +1190,15 @@
Clients - catégorie A34211
-
+
+ Clients - catégorie B34212
-
+
+
@@ -1370,7 +1372,7 @@
Divers débiteurs3488
-
+
@@ -1515,7 +1517,8 @@
Fournisseurs4411
-
+
+
@@ -1761,7 +1764,7 @@
Divers créanciers4488
-
+
@@ -2065,6 +2068,12 @@
+
+ Achats des prestations de service
+ 61263
+
+
+ Achats de matières et de fournitures des exercices antérieurs6128
diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py
index 2f6e3731faa..51ddb70c94c 100644
--- a/addons/l10n_multilang/models/l10n_multilang.py
+++ b/addons/l10n_multilang/models/l10n_multilang.py
@@ -12,6 +12,19 @@ _logger = logging.getLogger(__name__)
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
+ def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
+ res = super(AccountChartTemplate, self).load_for_current_company(sale_tax_rate, purchase_tax_rate)
+ # Copy chart of account translations when loading chart of account
+ for chart_template in self.filtered('spoken_languages'):
+ external_id = self.env['ir.model.data'].search([
+ ('model', '=', 'account.chart.template'),
+ ('res_id', '=', chart_template.id),
+ ], order='id', limit=1)
+ module = external_id and self.env.ref('base.module_' + external_id.module)
+ if module and module.state == 'installed':
+ chart_template.process_coa_translations()
+ return res
+
@api.multi
def process_translations(self, langs, in_field, in_ids, out_ids):
"""
diff --git a/addons/l10n_uk/__manifest__.py b/addons/l10n_uk/__manifest__.py
index 55168a9887c..fdd0323faf4 100644
--- a/addons/l10n_uk/__manifest__.py
+++ b/addons/l10n_uk/__manifest__.py
@@ -15,7 +15,7 @@ This is the latest UK Odoo localisation necessary to run Odoo accounting for UK
- InfoLogic UK counties listing
- a few other adaptations""",
'author': 'SmartMode LTD',
- 'website': 'http://www.smartmode.co.uk',
+ 'website': 'https://www.odoo.com/page/accounting',
'depends': [
'account',
'base_iban',
diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js
index f1acf81b301..c7e49ca38b6 100644
--- a/addons/lunch/static/src/js/lunch_kanban_controller.js
+++ b/addons/lunch/static/src/js/lunch_kanban_controller.js
@@ -117,9 +117,11 @@ var LunchKanbanController = KanbanController.extend({
* @private
*/
_updateSearchPanel: function () {
- var locationId = this.model.getCurrentLocationId();
- var domain = this.controlPanelDomain.concat([['is_available_at', 'in', [locationId]]]);
- return this._searchPanel.update({searchDomain: domain});
+ var self = this;
+ this.model.getLocationDomain().then(function (locationDomain) {
+ var domain = self.controlPanelDomain.concat(locationDomain);
+ return self._searchPanel.update({searchDomain: domain});
+ });
},
//--------------------------------------------------------------------------
diff --git a/addons/lunch/static/src/js/lunch_kanban_model.js b/addons/lunch/static/src/js/lunch_kanban_model.js
index c2386c7ace8..d94812a6c3c 100644
--- a/addons/lunch/static/src/js/lunch_kanban_model.js
+++ b/addons/lunch/static/src/js/lunch_kanban_model.js
@@ -12,6 +12,7 @@ var LunchKanbanModel = KanbanModel.extend({
init: function () {
this.locationId = false;
this.userId = false;
+ this._promInitLocation = null;
this._super.apply(this, arguments);
},
@@ -26,15 +27,23 @@ var LunchKanbanModel = KanbanModel.extend({
getCurrentLocationId: function () {
return this.locationId;
},
+ /**
+ * @return {Promise} resolved with the location domain
+ */
+ getLocationDomain: function () {
+ var self = this;
+ return this._initUserLocation().then(function () {
+ return self._buildLocationDomainLeaf() ? [self._buildLocationDomainLeaf()]: [];
+ });
+ },
load: function () {
var self = this;
var args = arguments;
var _super = this._super;
- return this._getUserLocation().then(function (locationId) {
+ return this._initUserLocation().then(function () {
var params = args[0];
- self.locationId = locationId;
- self._addOrUpdate(params.domain, ['is_available_at', 'in', [locationId]]);
+ self._addOrUpdate(params.domain, self._buildLocationDomainLeaf());
return _super.apply(self, args);
});
@@ -42,7 +51,7 @@ var LunchKanbanModel = KanbanModel.extend({
reload: function (id, options) {
var domain = options && options.domain || this.localData[id].domain;
- this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
+ this._addOrUpdate(domain, this._buildLocationDomainLeaf());
options = _.extend(options, {domain: domain});
return this._super.apply(this, arguments);
@@ -53,19 +62,35 @@ var LunchKanbanModel = KanbanModel.extend({
//--------------------------------------------------------------------------
_addOrUpdate: function (domain, subDomain) {
- var key = subDomain[0];
- var index = _.findIndex(domain, function (val) {
- return val[0] === key;
- });
+ if (subDomain && subDomain.length) {
+ var key = subDomain[0];
+ var index = _.findIndex(domain, function (val) {
+ return val[0] === key;
+ });
- if (index < 0) {
- domain.push(subDomain);
- } else {
- domain[index] = subDomain;
+ if (index < 0) {
+ domain.push(subDomain);
+ } else {
+ domain[index] = subDomain;
+ }
+
+ return domain;
}
return domain;
},
+ /**
+ * Builds the domain leaf corresponding to the current user's location
+ *
+ * @private
+ * @return {Array}
+ */
+ _buildLocationDomainLeaf: function () {
+ var locationId = this.getCurrentLocationId();
+ if (locationId) {
+ return ['is_available_at', 'in', [locationId]];
+ }
+ },
_getUserLocation: function () {
return this._rpc({
route: '/lunch/user_location_get',
@@ -74,18 +99,34 @@ var LunchKanbanModel = KanbanModel.extend({
},
});
},
+ /**
+ * Gets the user location once.
+ * Can be triggered from anywhere
+ * Useful to inject the location domain in the search panel
+ *
+ * @private
+ * @return {Promise}
+ */
+ _initUserLocation: function () {
+ var self = this;
+ if (!this._promInitLocation) {
+ this._promInitLocation = new Promise(function (resolve) {
+ self._getUserLocation().then(function (locationId) {
+ self.locationId = locationId;
+ resolve();
+ });
+ });
+ }
+ return this._promInitLocation;
+ },
_updateLocation: function (locationId) {
this.locationId = locationId;
return Promise.resolve();
},
_updateUser: function (userId) {
- var self = this;
-
this.userId = userId;
-
- return this._getUserLocation().then(function (locationId) {
- self.locationId = locationId;
- });
+ this._promInitLocation = null;
+ return this._initUserLocation();
}
});
diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js
index a0cd6d3f3e0..6be5e7f2c7b 100644
--- a/addons/lunch/static/src/js/lunch_kanban_view.js
+++ b/addons/lunch/static/src/js/lunch_kanban_view.js
@@ -18,6 +18,26 @@ var LunchKanbanView = KanbanView.extend({
Renderer: LunchKanbanRenderer,
}),
display_name: _lt('Lunch Kanban'),
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Injects an additional domain in the search panel
+ *
+ * @private
+ * @override
+ */
+ _createSearchPanel: function (parent) {
+ var self = this;
+ var model = this.getModel(parent);
+ var _super = this._super.bind(this);
+ return model.getLocationDomain().then(function (domain) {
+ self.loadParams.domain = self.loadParams.domain.concat(domain);
+ return _super(parent);
+ });
+ },
});
view_registry.add('lunch_kanban', LunchKanbanView);
diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js
index 8bc4879f9e5..8f64e7b0481 100644
--- a/addons/lunch/static/tests/lunch_kanban_tests.js
+++ b/addons/lunch/static/tests/lunch_kanban_tests.js
@@ -211,6 +211,56 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
+ QUnit.test('search panel domain location', async function (assert) {
+ assert.expect(10);
+ const locationId = this.data['lunch.location'].records[0].id;
+ const regularInfos = _.extend({}, this.regularInfos);
+
+ const kanban = await createLunchKanbanView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+
+
+
+
+
+
+
+ `,
+ mockRPC: function (route, args) {
+ assert.step(route);
+
+ if (route.startsWith('/lunch')) {
+ return mockLunchRPC({
+ infos: regularInfos,
+ userLocation: locationId,
+ }).apply(this, arguments);
+ }
+ if (args.method === 'search_panel_select_multi_range') {
+ assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [locationId]]],
+ 'The initial domain of the search panel must contain the user location');
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [["is_available_at", "in", [locationId]]],
+ 'The domain for fetching actual data should be correct');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.verifySteps([
+ '/lunch/user_location_get',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/call_kw/product/search_panel_select_multi_range',
+ '/web/dataset/search_read',
+ '/lunch/infos',
+ '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
+ ])
+
+ kanban.destroy();
+ });
+
QUnit.test('non-empty cart', async function (assert) {
assert.expect(17);
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index 39b7c53ddf5..0e786eb4c4c 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -144,7 +144,7 @@ class MailController(http.Controller):
@http.route('/mail/read_followers', type='json', auth='user')
def read_followers(self, follower_ids, res_model):
followers = []
- is_editable = request.env.user.has_group('base.group_no_one')
+ is_editable = request.env['mail.followers'].user_has_groups('base.group_no_one')
partner_id = request.env.user.partner_id
follower_id = None
follower_recs = request.env['mail.followers'].sudo().browse(follower_ids)
diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js
index 0ef02e5f57a..cb860b70beb 100644
--- a/addons/mail/static/src/js/models/threads/document_thread.js
+++ b/addons/mail/static/src/js/models/threads/document_thread.js
@@ -62,9 +62,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `close` operation comes from local storage event.
*/
- close: function () {
+ close: function (options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
windowState: 'closed',
@@ -75,9 +82,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `detach` operation comes from local storage event.
*/
- detach: function () {
+ detach: function (options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
var windowState = this._folded ? 'folded' : 'open';
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
@@ -105,9 +119,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `detach` operation comes from local storage event.
*/
- fold: function () {
+ fold: function (folded, options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
var windowState = this._folded ? 'folded' : 'open';
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
diff --git a/addons/mail/static/src/js/models/threads/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js
index 511bc6348c8..2b9d64b2597 100644
--- a/addons/mail/static/src/js/models/threads/searchable_thread.js
+++ b/addons/mail/static/src/js/models/threads/searchable_thread.js
@@ -148,7 +148,11 @@ var SearchableThread = Thread.extend({
if (cache.messages[index] !== message) {
cache.messages.splice(index, 0, message);
}
- if (!message.isMyselfAuthor() && options.incrementUnread) {
+ if (
+ !message.isMyselfAuthor() &&
+ options.incrementUnread &&
+ message.getType() !== 'notification'
+ ) {
this._incrementUnreadCounter();
}
},
diff --git a/addons/mail/static/src/js/services/mail_document_thread_manager.js b/addons/mail/static/src/js/services/mail_document_thread_manager.js
index acf06461a04..6bb66b1b439 100644
--- a/addons/mail/static/src/js/services/mail_document_thread_manager.js
+++ b/addons/mail/static/src/js/services/mail_document_thread_manager.js
@@ -21,17 +21,23 @@ MailManager.include({
start: function () {
this._super.apply(this, arguments);
-
+ this._documentThreadWindowStates = {};
+ this._tabId = this.call('bus_service', 'getTabId');
// retrieve the open DocumentThreads from the localStorage
- var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- if (!state) {
- this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {});
- } else {
- this.isReady().then(this._updateDocumentThreadWindows.bind(this, state));
+ var localStorageLength = this.call('local_storage', 'length');
+ for (var i = 0; i < localStorageLength; i++) {
+ var key = this.call('local_storage', 'key', i);
+ if (key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
+ // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
+ var documentThreadId = key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
+ this._documentThreadWindowStates[documentThreadId] =
+ this.call('local_storage', 'getItem', key);
+ }
}
+ this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
// listen to localStorage changes to synchronize DocumentThread's
// windows between tabs
- window.addEventListener('storage', this._onStorage.bind(this));
+ this.call('local_storage', 'onStorage', this, this._onStorage);
},
//--------------------------------------------------------------------------
@@ -60,7 +66,10 @@ MailManager.include({
options.postedFromDocumentThread
) {
var key = this.DOCUMENT_THREAD_MESSAGE_KEY;
- this.call('local_storage', 'setItem', key, data);
+ this.call('local_storage', 'setItem', key, {
+ messageData: data,
+ tabId: this._tabId,
+ });
}
return this._super.apply(this, arguments);
},
@@ -144,12 +153,11 @@ MailManager.include({
* @param {string} state.windowState ('closed', 'folded' or 'open')
*/
updateDocumentThreadState: function (threadID, state) {
- var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- states = _.omit(states, function (state) {
- return state.windowState === 'closed';
+ this._documentThreadWindowStates[threadID] = state;
+ this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY + '/' + threadID, {
+ state: state,
+ tabId: this._tabId,
});
- states[threadID] = state;
- this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states);
},
//--------------------------------------------------------------------------
@@ -194,10 +202,17 @@ MailManager.include({
resModel: info[0],
});
if (state.windowState === 'closed') {
- documentThread.close();
+ documentThread.close({
+ skipCrossTabSync: true,
+ });
} else {
- documentThread.fold(state.windowState === 'folded');
- self.openThreadWindow(documentThread.getID(), { keepFoldState: true });
+ documentThread.fold(state.windowState === 'folded', {
+ skipCrossTabSync: true,
+ });
+ self.openThreadWindow(documentThread.getID(), {
+ keepFoldState: true,
+ skipCrossTabSync: true,
+ });
}
});
},
@@ -213,13 +228,27 @@ MailManager.include({
* @param {StorageEvent} ev
*/
_onStorage: function (ev) {
- if (ev.key === this.DOCUMENT_THREAD_STATE_KEY) {
- var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- this._updateDocumentThreadWindows(state);
+ var value;
+ try {
+ value = JSON.parse(ev.newValue);
+ } catch (err) {
+ return;
+ }
+ if (ev.key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
+ if (value.tabId === this._tabId) {
+ return;
+ }
+ // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
+ var documentThreadId = ev.key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
+ var param = {};
+ param[documentThreadId] = value.state;
+ this._updateDocumentThreadWindows(param);
} else if (ev.key === this.DOCUMENT_THREAD_MESSAGE_KEY) {
- var message = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_MESSAGE_KEY);
- if (message) {
- this.addMessage(message);
+ if (value.tabId === this._tabId) {
+ return;
+ }
+ if (value.messageData) {
+ this.addMessage(value.messageData);
}
}
},
diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js
index a832b0b0793..d8174da5a73 100644
--- a/addons/mail/static/src/js/services/mail_window_manager.js
+++ b/addons/mail/static/src/js/services/mail_window_manager.js
@@ -94,6 +94,8 @@ MailManager.include({
* is not open yet, and do nothing otherwise.
* @param {boolean} [options.keepFoldState=false] if set to true, keep the
* fold state of the thread
+ * @param {boolean} [options.skipCrossTabSync=false] if set, thread
+ * should not notify other tabs from new document thread chat window state.
* @returns {Promise}
*/
openThreadWindow: function (threadID, options) {
@@ -132,7 +134,9 @@ MailManager.include({
// thread window could not be open, which may happen due to
// access error while fetching messages to the document.
// abort opening the thread window in this case.
- thread.close();
+ thread.close({
+ skipCrossTabSync: true,
+ });
thread.isCreatingWindow = false;
});
}
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
index 16d5cd31c86..08671354a3e 100644
--- a/addons/mail/static/src/js/thread_windows/thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -3,7 +3,9 @@ odoo.define('mail.ThreadWindow', function (require) {
var AbstractThreadWindow = require('mail.AbstractThreadWindow');
var BasicComposer = require('mail.composer.Basic');
+var ExtendedComposer = require('mail.composer.Extended');
+var config = require('web.config');
var core = require('web.core');
var QWeb = core.qweb;
@@ -65,19 +67,27 @@ var ThreadWindow = AbstractThreadWindow.extend({
if (!this.hasThread()) {
this._startWithoutThread();
} else if (this.needsComposer()) {
- var basicComposer = new BasicComposer(this, {
+ var composer;
+ var composerParams = {
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
- isMini: true,
thread: this._thread,
- });
- basicComposer.on('post_message', this, this._postMessage);
- basicComposer.once('input_focused', this, function () {
+ };
+ if (
+ config.device.isMobile &&
+ this._thread.isMassMailing()
+ ) {
+ composer = new ExtendedComposer(this, composerParams);
+ } else {
+ composer = new BasicComposer(this, _.extend({ isMini: true }, composerParams));
+ }
+ composer.on('post_message', this, this._postMessage);
+ composer.once('input_focused', this, function () {
var commands = this._thread.getCommands();
var partners = this._thread.getMentionPartnerSuggestions();
- basicComposer.mentionSetCommands(commands);
- basicComposer.mentionSetPrefetchedPartners(partners);
+ composer.mentionSetCommands(commands);
+ composer.mentionSetPrefetchedPartners(partners);
});
- composerDef = basicComposer.replace(this.$('.o_thread_composer'));
+ composerDef = composer.replace(this.$('.o_thread_composer'));
composerDef.then(function () {
self.$input = self.$('.o_composer_text_field');
});
@@ -129,24 +139,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
isPassive: function () {
return this._passive;
},
- /**
- * States whether the input of the thread window should be displayed or not.
- * This is based on the type of the thread:
- *
- * Do not display the input in the following cases:
- *
- * - no thread related to this window
- * - window of a mailbox (temp: let us have mailboxes in window mode)
- * - window of a thread with mass mailing
- *
- * Any other threads show the input in the window.
- *
- * @override
- * @returns {boolean}
- */
- needsComposer: function () {
- return this._super() && !this._thread.isMassMailing();
- },
/**
* Turn the thread window in active mode, so that when the bottom of the
* thread is visible, it is automatically marked as read.
diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js
index 14ac97def76..25e327b0428 100644
--- a/addons/mail/static/src/js/utils.js
+++ b/addons/mail/static/src/js/utils.js
@@ -8,7 +8,12 @@ var _t = core._t;
function parseAndTransform(htmlString, transformFunction) {
var openToken = "OPEN" + Date.now();
var string = htmlString.replace(/</g, openToken);
- var children = $('
').html(string).contents();
+ var children;
+ try {
+ children = $('
').html(string).contents();
+ } catch (e) {
+ children = $('
').html('
' + string + '
').contents();
+ }
return _parseAndTransform(children, transformFunction)
.replace(new RegExp(openToken, "g"), "<");
}
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index 90793c91e95..5a9bad8e2dc 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -251,6 +251,9 @@
.o_composer_send .o_composer_button_send, .o_chatter_avatar {
display: none !important;
}
+ .o_attachment {
+ width: 50%;
+ }
}
.o_file_drop_zone_container {
@@ -292,6 +295,10 @@
flex-flow: row wrap;
}
+ .o_attachment_wrap {
+ padding-right: 0px;
+ }
+
.o_attachments_previews {
> .o_attachment {
width: 50%;
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 89e5f220ea2..8adf2113b47 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -183,6 +183,7 @@
}
img {
max-width: 100%;
+ height: auto;
}
.o_mail_body_long {
diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js
index 84fd155522c..8f14f703b5d 100644
--- a/addons/mail/static/tests/discuss_mobile_tests.js
+++ b/addons/mail/static/tests/discuss_mobile_tests.js
@@ -13,6 +13,9 @@ QUnit.module('Discuss in mobile', {
beforeEach: function () {
this.services = mailTestUtils.getMailServices();
this.data = {
+ 'mail.channel': {
+ fields: {},
+ },
'mail.message': {
fields: {},
},
@@ -80,27 +83,65 @@ QUnit.test('mobile basic rendering', async function (assert) {
discuss.destroy();
});
-QUnit.test('on_{attach/detach}_callback', function (assert) {
+QUnit.test('on_{attach/detach}_callback', async function (assert) {
assert.expect(2);
- var done = assert.async();
- createDiscuss({
+ var discuss = await createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
- }).then(function (discuss) {
- try {
- discuss.on_attach_callback();
- assert.ok(true, 'should not crash on attach callback');
- discuss.on_detach_callback();
- assert.ok(true, 'should not crash on detach callback');
- } finally {
- discuss.destroy();
- done();
- }
});
+
+ discuss.on_attach_callback();
+ assert.ok(true, 'should not crash on attach callback');
+ discuss.on_detach_callback();
+ assert.ok(true, 'should not crash on detach callback');
+ discuss.destroy();
+});
+
+QUnit.test('extended composer in mass mailing channel', async function (assert) {
+ assert.expect(4);
+
+ this.data.initMessaging = {
+ channel_slots: {
+ channel_channel: [{
+ id: 1,
+ channel_type: "channel",
+ name: "General",
+ mass_mailing: true,
+ }],
+ },
+ };
+
+ var discuss = await createDiscuss({
+ id: 1,
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ });
+
+ await testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type="multi_user_channel"]'));
+ await testUtils.dom.click(discuss.$('.o_mail_preview[data-preview-id="1"]'));
+ assert.containsOnce(
+ $,
+ '.o_thread_window',
+ "should display a chat window");
+ assert.strictEqual(
+ $('.o_thread_window').data('thread-id'),
+ 1,
+ "chat window should be from channel General");
+ assert.containsOnce(
+ $('.o_thread_window'),
+ '.o_thread_composer',
+ "chat window should have a composer");
+ assert.ok(
+ $('.o_thread_window .o_thread_composer').hasClass('o_thread_composer_extended'),
+ "chat window composer should be extended composer");
+
+ discuss.destroy();
});
});
diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js
index 86d06ba36c4..0748168a0cb 100644
--- a/addons/mail/static/tests/document_thread_window_tests.js
+++ b/addons/mail/static/tests/document_thread_window_tests.js
@@ -201,7 +201,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem' &&
ev.data.args[0] === 'mail.document_threads_last_message') {
- assert.deepEqual(ev.data.args[1], newMessage,
+ assert.deepEqual(ev.data.args[1].messageData, newMessage,
"should write sent message in local storage, to share info with other tabs");
}
}, true);
@@ -227,7 +227,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
});
QUnit.test('open, fold, unfold and close a document thread window', async function (assert) {
- assert.expect(8);
+ assert.expect(24);
var messagingMenu = new MessagingMenu();
testUtils.mock.addMockEnvironment(messagingMenu, {
services: this.services,
@@ -237,7 +237,13 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
await testUtils.nextTick();
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') {
- assert.step(JSON.stringify(ev.data.args));
+ const key = ev.data.args[0];
+ const state = ev.data.args[1].state;
+ assert.strictEqual(key, 'mail.document_threads_state/some.res.model_1');
+ assert.ok(state);
+ assert.ok(state.name);
+ assert.ok(state.windowState);
+ assert.step(`${key}: { name: "${state.name}", windowState: '${state.windowState}' }`);
}
}, true);
await messagingMenu.appendTo($('#qunit-fixture'));
@@ -263,10 +269,10 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
await testUtils.nextTick();
assert.verifySteps([
- '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
- '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"folded"}}]',
- '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
- '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"closed"}}]',
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'folded' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'closed' }`,
]);
messagingMenu.destroy();
diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
index 610e689ae4a..047d723a425 100644
--- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
@@ -427,6 +427,88 @@ QUnit.test('update messaging preview on receiving a new message in channel previ
messagingMenu.destroy();
});
+QUnit.test('new message of type "notification" are not considered as unread messages', async function (assert) {
+ assert.expect(8);
+
+ const messagingMenu = new MessagingMenu();
+ testUtils.mock.addMockEnvironment(messagingMenu, {
+ data: this.data,
+ services: this.services,
+ });
+ await messagingMenu.appendTo($('#qunit-fixture'));
+ // open messaging menu
+ await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
+ assert.containsOnce(
+ messagingMenu,
+ '.o_mail_preview',
+ "should display a single channel preview");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_preview_name')
+ .text()
+ .trim(),
+ "general",
+ "should display channel preview of 'general' channel");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_preview_counter')
+ .text()
+ .trim(),
+ "",
+ "should have unread counter of 0 for 'general' channel (no unread messages)");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_last_message_preview')
+ .text()
+ .replace(/\s/g, ""),
+ "Me:test",
+ "should display author name and inline body of currently last message in the channel");
+ // close messaging menu
+ await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
+
+ // simulate receiving a new message on the channel 'general' (id 1)
+ // of type 'notification'
+ const data = {
+ id: 100,
+ author_id: [42, "Someone"],
+ body: "
Left #general
",
+ channel_ids: [1],
+ message_type: 'notification',
+ };
+ const notification = [[false, 'mail.channel', 1], data];
+ messagingMenu.call('bus_service', 'trigger', 'notification', [notification]);
+ await testUtils.nextTick();
+ // open messaging menu
+ await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
+ assert.containsOnce(
+ messagingMenu,
+ '.o_mail_preview',
+ "should still display a single channel preview");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_preview_name')
+ .text()
+ .trim(),
+ "general",
+ "should still display channel preview of 'general' channel");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_preview_counter')
+ .text()
+ .trim(),
+ "",
+ "should still have unread counter of 0 for 'general' channel (no unread messages)");
+ assert.strictEqual(
+ messagingMenu
+ .$('.o_last_message_preview')
+ .text()
+ .replace(/\s/g, ""),
+ "Someone:Left#general",
+ "should display author name and inline body of notification message (last message of channel)");
+
+ messagingMenu.destroy();
+});
+
QUnit.test('preview of inbox message not linked to document + mark as read', async function (assert) {
assert.expect(17);
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index 14f17c71c3e..f652e0b6575 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -128,8 +128,8 @@
-
Category:
-
Request to:
+
Category:
+
Request to:
@@ -427,8 +427,8 @@
-
Serial Number:
-
Model Number:
+
Serial Number:
+
Model Number:
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 93db2240207..76e547c6916 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -338,6 +338,7 @@ class MrpProduction(models.Model):
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids)
else:
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)
+ order.post_visible &= all(wo.state in ['done', 'cancel'] for wo in order.workorder_ids) or all(m.product_id.tracking == 'none' for m in order.move_raw_ids)
@api.multi
@api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
@@ -619,6 +620,8 @@ class MrpProduction(models.Model):
@api.multi
def open_produce_product(self):
self.ensure_one()
+ if self.bom_id.type == 'phantom':
+ raise UserError(_('You cannot produce a MO with a bom kit product.'))
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
return action
diff --git a/addons/mrp/models/res_config_settings.py b/addons/mrp/models/res_config_settings.py
index 087d8050b40..41cf3015089 100644
--- a/addons/mrp/models/res_config_settings.py
+++ b/addons/mrp/models/res_config_settings.py
@@ -15,7 +15,6 @@ class ResConfigSettings(models.TransientModel):
module_mrp_plm = fields.Boolean("Product Lifecycle Management (PLM)")
module_mrp_workorder = fields.Boolean("Work Orders")
module_quality_control = fields.Boolean("Quality")
- module_mrp_subcontracting = fields.Boolean("Subcontracting")
group_mrp_routings = fields.Boolean("MRP Work Orders",
implied_group='mrp.group_mrp_routings')
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index f2058420815..a65cf3ca9fb 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -233,6 +233,11 @@ class StockMove(models.Model):
res = super(StockMove, self)._should_be_assigned()
return bool(res and not (self.production_id or self.raw_material_production_id))
+ def _key_assign_picking(self):
+ keys = super(StockMove, self)._key_assign_picking()
+ return keys + (self.created_production_id,)
+
+
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
""" Computes the quantity delivered or received when a kit is sold or purchased.
A ratio 'qty_processed/qty_needed' is computed for each component, and the lowest one is kept
@@ -266,4 +271,4 @@ class StockMove(models.Model):
# doesn't make sense.
return min(qty_ratios) // 1
else:
- return 0.0
+ return 0.0
\ No newline at end of file
diff --git a/addons/mrp/views/res_config_settings_views.xml b/addons/mrp/views/res_config_settings_views.xml
index a40a1d150ee..96378ad693b 100644
--- a/addons/mrp/views/res_config_settings_views.xml
+++ b/addons/mrp/views/res_config_settings_views.xml
@@ -44,17 +44,6 @@
-
-
-
-
-
-
-
- Subcontract the production of some products
-
-
-
Products
diff --git a/addons/mrp_subcontracting/__init__.py b/addons/mrp_subcontracting/__init__.py
deleted file mode 100644
index be29e5c5628..00000000000
--- a/addons/mrp_subcontracting/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import models
-from odoo import api, SUPERUSER_ID
-
-def _create_subcontracting_rules(cr, registry):
- """ This hook is used to add the default subcontracting rules on every warehouse/company. It is
- necessary if the mrp_subcontracting module is installed after some warehouses
- were already created.
- """
- env = api.Environment(cr, SUPERUSER_ID, {})
- warehouse_ids = env['stock.warehouse'].search(['|', ('subcontracting_mto_pull_id', '=', False), ('subcontracting_pull_id', '=', False)])
- for warehouse_id in warehouse_ids:
- warehouse_id.write({'subcontracting_to_resupply': warehouse_id.subcontracting_to_resupply})
- company_ids = env['res.company'].search([('subcontracting_location_id', '!=', False)])
- for company_id in company_ids:
- wh = env['stock.warehouse'].search([('company_id', '=', company_id.id), ('active', '=', True)], limit=1)
- production_location = wh._get_production_location()
- rsoo_rule = env['stock.rule'].search([
- ('location_src_id', '=', company_id.subcontracting_location_id.id),
- ('location_id', '=', production_location.id),
- ('picking_type_id', '=', wh.manu_type_id.id)
- ])
- if not rsoo_rule:
- company_id._create_resupply_subcontractor_rules()
diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py
deleted file mode 100644
index d6fbd927bdb..00000000000
--- a/addons/mrp_subcontracting/__manifest__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-{
- 'name': "mrp_subcontracting",
- 'version': '0.1',
- 'summary': "Subcontract Productions",
- 'description': "",
- 'website': 'https://www.odoo.com/page/manufacturing',
- 'category': 'Manufacturing/Manufacturing',
- 'depends': ['mrp', 'contacts'],
- 'data': [
- 'data/mrp_subcontracting_data.xml',
- 'views/stock_warehouse_views.xml',
- 'views/stock_picking_views.xml',
- 'views/mrp_bom_views.xml',
- ],
- 'post_init_hook': '_create_subcontracting_rules',
-}
diff --git a/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml b/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml
deleted file mode 100644
index 3966da5b9f2..00000000000
--- a/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- Resupply Subcontractor on Order
-
- 5
-
-
-
-
-
diff --git a/addons/mrp_subcontracting/models/__init__.py b/addons/mrp_subcontracting/models/__init__.py
deleted file mode 100644
index 98238bd9a1b..00000000000
--- a/addons/mrp_subcontracting/models/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import mrp_bom
-from . import res_company
-from . import res_partner
-from . import stock_move
-from . import stock_picking
-from . import stock_warehouse
-
diff --git a/addons/mrp_subcontracting/models/mrp_bom.py b/addons/mrp_subcontracting/models/mrp_bom.py
deleted file mode 100644
index 8f0d87d0b73..00000000000
--- a/addons/mrp_subcontracting/models/mrp_bom.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import fields, models
-
-
-class MrpBom(models.Model):
- _inherit = 'mrp.bom'
-
- type = fields.Selection(selection_add=[('subcontract', 'Subcontracting')])
- subcontractor_ids = fields.One2many('res.partner', 'bom_id', domain=[('type', '=', 'subcontractor')], string='Subcontractors')
-
- def _bom_subcontract_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False):
- domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type)
- if subcontractor:
- domain += ['|', ('subcontractor_ids', 'in', subcontractor.id), ('subcontractor_ids', '=', False)]
- else:
- domain += [('subcontractor_ids', '=', False)]
- return self.search(domain, order='subcontractor_ids, sequence, product_id', limit=1)
-
diff --git a/addons/mrp_subcontracting/models/res_company.py b/addons/mrp_subcontracting/models/res_company.py
deleted file mode 100644
index 93687fa4d27..00000000000
--- a/addons/mrp_subcontracting/models/res_company.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import api, fields, models, _
-
-
-class ResCompany(models.Model):
- _inherit = 'res.company'
-
- subcontracting_location_id = fields.Many2one('stock.location')
-
- def _create_per_company_locations(self):
- super(ResCompany, self)._create_per_company_locations()
- self._create_subcontracting_location()
-
- def _create_subcontracting_location(self):
- for company in self:
- parent_location = self.env.ref('stock.stock_location_locations_partner', raise_if_not_found=False)
- subcontracting_location = self.env['stock.location'].create({
- 'name': _('%s: Subcontracting Location') % company.name,
- 'usage': 'internal',
- 'location_id': parent_location.id,
- 'company_id': company.id,
- })
- company.write({'subcontracting_location_id': subcontracting_location.id})
-
- @api.model
- def create_missing_subcontracting_location(self):
- company_without_subcontracting_loc = self.env['res.company'].search(
- [('subcontracting_location_id', '=', False)])
- for company in company_without_subcontracting_loc:
- company._create_subcontracting_location()
-
- def _create_resupply_subcontractor_rules(self):
- self.ensure_one()
- wh = self.env['stock.warehouse'].search([('company_id', '=', self.id), ('active', '=', True)], limit=1)
- subcontracting_location = self.subcontracting_location_id
- production_location = wh._get_production_location()
- resupply_route = wh._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto', _('Resupply Subcontractor on Order'))
- resupply_route.rule_ids = [(0, 0, {
- 'name': '%s → %s' % (subcontracting_location.name, production_location.name),
- 'action': 'pull',
- 'picking_type_id': wh.subcontracting_type_id,
- 'location_src_id': subcontracting_location,
- 'location_id': production_location,
- })],
-
diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py
deleted file mode 100644
index 925296fee0c..00000000000
--- a/addons/mrp_subcontracting/models/res_partner.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import api, fields, models, _
-
-
-class ResPartner(models.Model):
- _inherit = 'res.partner'
-
- type = fields.Selection(selection_add=[('subcontractor', 'Subcontractor')])
- bom_id = fields.Many2one('mrp.bom', domain=[('type', '=', 'subcontract')])
-
- def _get_name(self):
- name = super(ResPartner, self)._get_name()
- if self.type == 'subcontractor':
- subcontractor = _('Subcontractor')
- if name.endswith(', '):
- name = "%s%s" % (name, subcontractor)
- else:
- name = '%s, %s' % (name, subcontractor)
- return name
-
- @api.onchange('type')
- def _onchange_partner_type(self):
- if self.type == 'subcontractor':
- company = self.company_id or self.parent_id.company_id
- subcontracting_location = company.subcontracting_location_id
- self.update({
- 'property_stock_customer': subcontracting_location,
- 'property_stock_supplier': subcontracting_location
- })
- else:
- self.update({
- 'property_stock_customer': self.env.ref('stock.stock_location_customers'),
- 'property_stock_supplier': self.env.ref('stock.stock_location_suppliers')
- })
-
- @api.model_create_multi
- def create(self, vals_list):
- for vals in vals_list:
- if vals.get('type') == 'subcontractor':
- if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
- company = vals.get('company_id') and self.env['res.company'].browse(vals['company_id']) or \
- self.browse(vals.get('parent_id')).company_id
- subcontracting_location = company.subcontracting_location_id
- vals['property_stock_supplier'] = subcontracting_location
- vals['property_stock_customer'] = subcontracting_location
- return super(ResPartner, self).create(vals_list)
-
- def write(self, vals):
- if vals.get('type') == 'subcontractor':
- subcontracting_location = self.env.company.subcontracting_location_id
- if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
- vals['property_stock_supplier'] = subcontracting_location.id
- vals['property_stock_customer'] = subcontracting_location.id
- return super(ResPartner, self).write(vals)
diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py
deleted file mode 100644
index daf847891bf..00000000000
--- a/addons/mrp_subcontracting/models/stock_move.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from collections import defaultdict
-
-from odoo import models, _
-from odoo.exceptions import UserError
-
-
-class StockMove(models.Model):
- _inherit = 'stock.move'
-
- def _get_subcontract_bom(self):
- self.ensure_one()
- bom = self.env['mrp.bom']._bom_subcontract_find(
- product=self.product_id,
- picking_type=self.picking_type_id,
- company_id=self.company_id.id,
- bom_type='subcontract',
- subcontractor=self.picking_id.partner_id,
- )
- return bom
-
- def _action_confirm(self, merge=True, merge_into=False):
- res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
- subcontract_details_per_picking = defaultdict(list)
- for move in res:
- if not move.picking_id:
- continue
- if not move.picking_id._is_subcontract():
- continue
- bom = move._get_subcontract_bom()
- if not bom:
- error_message = _('Please define a BoM of type subcontracting for the product "%s". If you don\'t want to subcontract the product "%s", do not select a partner of type subcontractor.')
- error_message += '\n\n'
- error_message += _('If there is well a BoM of type subcontracting defined, check if you have set the correct subcontractors on it.')
- raise UserError(error_message % (move.product_id.name, move.product_id.name))
- subcontract_details_per_picking[move.picking_id].append((move, bom))
- for picking, subcontract_details in subcontract_details_per_picking.items():
- picking._subcontracted_produce(subcontract_details)
- return res
diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py
deleted file mode 100644
index b3c93fb8b7e..00000000000
--- a/addons/mrp_subcontracting/models/stock_picking.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import api, fields, models
-
-
-class StockPicking(models.Model):
- _inherit = 'stock.picking'
-
- display_action_record_components = fields.Boolean(compute='_compute_display_action_record_components')
- display_view_subcontracted_move_lines = fields.Boolean(compute='_compute_display_view_subcontracted_move_lines')
-
- @api.depends('state')
- def _compute_display_action_record_components(self):
- for picking in self:
- # Hide if not encoding state
- if picking.state in ('draft', 'cancel', 'done'):
- continue
- if not picking._is_subcontract():
- continue
- # Hide if no move is tracked
- subcontracted_productions = picking._get_subcontracted_productions()
- subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
- subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
- if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
- continue
- # Hide if the production is to close
- if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
- continue
- picking.display_action_record_components = True
-
- @api.depends('state')
- def _compute_display_view_subcontracted_move_lines(self):
- for picking in self:
- # Hide if not encoding state
- if picking.state in ('draft', 'cancel'):
- continue
- if not picking._is_subcontract():
- continue
- # Hide until state done if no move is tracked, if tracked until something was produced
- subcontracted_productions = picking._get_subcontracted_productions()
- subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
- subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
- if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
- if picking.state != 'done':
- continue
- # Hide if nothing was produced
- if all(subcontracted_move.quantity_done == 0 for subcontracted_move in subcontracted_moves):
- continue
- picking.display_view_subcontracted_move_lines = True
-
- # -------------------------------------------------------------------------
- # Action methods
- # -------------------------------------------------------------------------
- def action_done(self):
- for picking in self:
- subcontracted_productions = picking._get_subcontracted_productions()
- for subcontracted_production in subcontracted_productions:
- subcontracted_production.button_mark_done()
- return super(StockPicking, self).action_done()
-
- def action_view_subcontracted_move_lines(self):
- """ Returns a list view with the move lines of the subcontracted products. To find them, we
- look on the origin moves of the move lines of the picking if there is a manufacturing order.
- """
- self.ensure_one()
- subcontracted_productions = self._get_subcontracted_productions()
- subcontracted_move_lines = self.env['stock.move.line']
- for subcontracted_production in subcontracted_productions:
- subcontracted_move_lines |= subcontracted_production.move_raw_ids.mapped('move_line_ids')
- subcontracted_move_lines |= subcontracted_production.move_finished_ids.mapped('move_line_ids')
- action = self.env.ref('stock.stock_move_line_action').read()[0]
- action['context'] = {}
- action['domain'] = [('id', 'in', subcontracted_move_lines.ids)]
- return action
-
- def action_record_components(self):
- self.ensure_one()
- subcontracted_productions = self._get_subcontracted_productions()
- to_register = subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done'))
- if to_register:
- mo = to_register[0]
- action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
- action['context'] = dict(self.env.context, active_id=mo.id)
- return action
-
- # -------------------------------------------------------------------------
- # Subcontract helpers
- # -------------------------------------------------------------------------
- def _is_subcontract(self):
- self.ensure_one()
- if self.partner_id.type == 'subcontractor' and \
- self.picking_type_id.code == 'incoming':
- return True
- return False
-
- def _get_subcontracted_productions(self):
- self.ensure_one()
- return self.move_lines.mapped('move_orig_ids.production_id')
-
- def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
- subcontract_move.ensure_one()
- product = subcontract_move.product_id
- warehouse = subcontract_move.warehouse_id or self.picking_type_id.warehouse_id
- vals = {
- 'product_id': product.id,
- 'product_uom_id': subcontract_move.product_uom.id,
- 'bom_id': bom.id,
- 'location_src_id': subcontract_move.picking_id.partner_id.property_stock_supplier.id,
- 'location_dest_id': subcontract_move.location_id.id,
- 'product_qty': subcontract_move.product_uom_qty,
- 'picking_type_id': warehouse.subcontracting_type_id.id
- }
- return vals
-
- def _subcontracted_produce(self, subcontract_details):
- self.ensure_one()
- for move, bom in subcontract_details:
- mo = self.env['mrp.production'].create(self._prepare_subcontract_mo_vals(move, bom))
- self.env['stock.move'].create(mo._get_moves_raw_values())
- mo.action_confirm()
-
- # Link the finished to the receipt move.
- finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
- finished_move.write({'move_dest_ids': [(4, move.id, False)]})
- mo.action_assign()
-
- # Only skip the produce wizard if no moves is tracked
- moves = mo.mapped('move_raw_ids') + mo.mapped('move_finished_ids')
- if any(move.has_tracking != 'none' for move in moves):
- continue
- for m in moves:
- m.quantity_done = m.product_uom_qty
diff --git a/addons/mrp_subcontracting/models/stock_warehouse.py b/addons/mrp_subcontracting/models/stock_warehouse.py
deleted file mode 100644
index a9d0cd1713b..00000000000
--- a/addons/mrp_subcontracting/models/stock_warehouse.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import api, fields, models, _
-
-
-class StockWarehouse(models.Model):
- _inherit = 'stock.warehouse'
-
- subcontracting_to_resupply = fields.Boolean(
- 'Resupply Subcontractors', default=True,
- help="Resupply subcontractors with components")
-
- subcontracting_mto_pull_id = fields.Many2one(
- 'stock.rule', 'Subcontracting MTO Rule')
- subcontracting_pull_id = fields.Many2one(
- 'stock.rule', 'Subcontracting MTS Rule'
- )
-
- subcontracting_route_id = fields.Many2one('stock.location.route', 'Resupply Subcontractor', ondelete='restrict')
-
- subcontracting_type_id = fields.Many2one(
- 'stock.picking.type', 'Subcontracting Operation Type',
- domain=[('code', '=', 'mrp_operation')])
-
- def get_rules_dict(self):
- result = super(StockWarehouse, self).get_rules_dict()
- subcontract_location_id = self._get_subcontracting_location()
- for warehouse in self:
- result[warehouse.id].update({
- 'subcontract': [
- self.Routing(warehouse.lot_stock_id, subcontract_location_id, warehouse.out_type_id, 'pull'),
- ]
- })
- return result
-
- def _get_routes_values(self):
- routes = super(StockWarehouse, self)._get_routes_values()
- routes.update({
- 'subcontracting_route_id': {
- 'routing_key': 'subcontract',
- 'depends': ['subcontracting_to_resupply'],
- 'route_create_values': {
- 'product_categ_selectable': False,
- 'warehouse_selectable': True,
- 'product_selectable': False,
- 'company_id': self.company_id.id,
- 'sequence': 10,
- 'name': self._format_routename(name=_('Resupply Subcontractor'))
- },
- 'route_update_values': {
- 'active': self.subcontracting_to_resupply,
- },
- 'rules_values': {
- 'active': self.subcontracting_to_resupply,
- }
- }
- })
- return routes
-
- def _get_global_route_rules_values(self):
- rules = super(StockWarehouse, self)._get_global_route_rules_values()
- subcontract_location_id = self._get_subcontracting_location()
- production_location_id = self._get_production_location()
- rules.update({
- 'subcontracting_mto_pull_id': {
- 'depends': ['subcontracting_to_resupply'],
- 'create_values': {
- 'procure_method': 'make_to_order',
- 'company_id': self.company_id.id,
- 'action': 'pull',
- 'auto': 'manual',
- 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
- 'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, 'MTO'),
- 'location_id': subcontract_location_id.id,
- 'location_src_id': self.lot_stock_id.id,
- 'picking_type_id': self.out_type_id.id
- },
- 'update_values': {
- 'active': self.subcontracting_to_resupply
- }
- },
- 'subcontracting_pull_id': {
- 'depends': ['subcontracting_to_resupply'],
- 'create_values': {
- 'procure_method': 'make_to_order',
- 'company_id': self.company_id.id,
- 'action': 'pull',
- 'auto': 'manual',
- 'route_id': self._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
- _('Resupply Subcontractor on Order')).id,
- 'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, False),
- 'location_id': production_location_id.id,
- 'location_src_id': subcontract_location_id.id,
- 'picking_type_id': self.out_type_id.id
- },
- 'update_values': {
- 'active': self.subcontracting_to_resupply
- }
- },
- })
- return rules
-
- def _get_picking_type_create_values(self, max_sequence):
- data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
- data.update({
- 'subcontracting_type_id': {
- 'name': _('Subcontracting'),
- 'code': 'mrp_operation',
- 'use_create_lots': True,
- 'use_existing_lots': True,
- 'sequence': next_sequence + 2
- },
- })
- return data, max_sequence + 4
-
- def _get_sequence_values(self):
- values = super(StockWarehouse, self)._get_sequence_values()
- values.update({
- 'subcontracting_type_id': {'name': self.name + ' ' + _('Sequence subcontracting'), 'prefix': self.code + '/SBC/', 'padding': 5},
- })
- return values
-
- def _get_picking_type_update_values(self):
- data = super(StockWarehouse, self)._get_picking_type_update_values()
- subcontract_location_id = self._get_subcontracting_location()
- production_location_id = self._get_production_location()
- data.update({
- 'subcontracting_type_id': {
- 'active': False,
- 'default_location_src_id': subcontract_location_id.id,
- 'default_location_dest_id': production_location_id.id,
- },
- })
- return data
-
- def _get_subcontracting_location(self):
- return self.company_id.subcontracting_location_id
-
diff --git a/addons/mrp_subcontracting/tests/__init__.py b/addons/mrp_subcontracting/tests/__init__.py
deleted file mode 100644
index c8d5649e39d..00000000000
--- a/addons/mrp_subcontracting/tests/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import test_subcontracting
-
diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py
deleted file mode 100644
index 032f319284a..00000000000
--- a/addons/mrp_subcontracting/tests/test_subcontracting.py
+++ /dev/null
@@ -1,526 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo.tests import Form
-from odoo.tests.common import SavepointCase, TransactionCase
-
-from odoo.tests import tagged
-
-@tagged('post_install', '-at_install')
-class TestSubcontractingBasic(TransactionCase):
- def test_partner_1(self):
- """ Checks the `display_name` of subcontracted partners. """
- main_partner = self.env['res.partner'].create({'name': 'main_partner'})
-
- # When a name is set to the subcontracted partner.
- subcontractor_partner1 = self.env['res.partner'].create({
- 'name': 'subcontractor_partner',
- 'type': 'subcontractor',
- 'parent_id': main_partner.id,
- })
- self.assertEqual(subcontractor_partner1.display_name, "main_partner, subcontractor_partner, Subcontractor")
-
- # When a name is not set to the subcontracted partner.
- subcontractor_partner1 = self.env['res.partner'].create({
- 'name': '',
- 'type': 'subcontractor',
- 'parent_id': main_partner.id,
- })
- self.assertEqual(subcontractor_partner1.display_name, "main_partner, Subcontractor")
-
- def test_partner_2(self):
- """ Open an existing partner and add a contact of type subcontractor (simple form view).
- Checks the customer and supplier locations are set to the subcontracting one onces saved.
- """
- that_company = self.env['res.partner'].create({
- 'is_company': True,
- 'name': "That Company",
- })
- partner_form = Form(that_company)
- with partner_form.child_ids.new() as partner:
- partner.name = 'That Guy'
- partner.type = 'subcontractor'
- partner_form.save()
- subcontractor = that_company.child_ids
- self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
- self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
-
- def test_partner_3(self):
- """ Create a new partner, select a parent company, and choose type subcontractor (debug
- active). Checks the onchange correctly set the customer and supplier locations to the
- subcontracting ones. Change them to another one, save, checks they are rightly applied.
- """
- # FIXME sle: this field should not be writeable without the debug mode but it is. ask xmo
- group_no_one = self.env.ref('base.group_no_one')
- self.env.user.write({'groups_id': [(3, group_no_one.id)]})
- partner_form = Form(self.env['res.partner'])
- partner_form.name = 'subcontractor partner'
- self.assertNotEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
- self.assertNotEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
- partner_form.type = 'subcontractor'
- self.assertEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
- self.assertEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
- subcontracting_location2 = self.env.user.company_id.subcontracting_location_id.copy()
- partner_form.property_stock_supplier = subcontracting_location2
- partner_form.property_stock_customer = subcontracting_location2
- partner = partner_form.save()
- self.assertEqual(partner.property_stock_supplier, subcontracting_location2)
- self.assertEqual(partner.property_stock_customer, subcontracting_location2)
-
- def test_partner_4(self):
- """ Create a new partner, select a parent company, and choose type subcontractor (debug
- not active). Checks the customer and supplier locations are set to the subcontracting one
- onces saved.
- """
- that_company = self.env['res.partner'].create({
- 'is_company': True,
- 'name': "That Company",
- })
- partner_form = Form(self.env['res.partner'])
- partner_form.name = 'That Guy'
- partner_form.parent_id = that_company
- partner_form.type = 'subcontractor'
- subcontractor = partner_form.save()
- self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
- self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
-
- def test_partner_5(self):
- """ Create a new partner with a child partner of type contact,
- then write the type 'subcontractor' on it
- """
- that_company = self.env['res.partner'].create({
- 'is_company': True,
- 'name': "That Company",
- })
- partner_form = Form(that_company)
- with partner_form.child_ids.new() as partner:
- partner.name = 'That Guy'
- partner.type = 'contact'
- partner_form.save()
-
- # We write a new type on the created child partner
- subcontractor = that_company.child_ids
- self.assertEquals(len(that_company.child_ids), 1)
- subcontractor.type = 'subcontractor'
-
- self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
- self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
-
- def test_subcontracting_location_1(self):
- """ Checks the creation and presence of the subcontracting location. """
- self.assertTrue(self.env.user.company_id.subcontracting_location_id)
- self.assertTrue(self.env.user.company_id.subcontracting_location_id.active)
- company2 = self.env['res.company'].create({'name': 'Test Company'})
- self.assertTrue(company2.subcontracting_location_id)
-
-
-class TestSubcontractingFlows(SavepointCase):
- @classmethod
- def setUpClass(cls):
- super(TestSubcontractingFlows, cls).setUpClass()
- # 1: Create a subcontracting partner
- main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
- cls.subcontractor_partner1 = cls.env['res.partner'].create({
- 'name': 'subcontractor_partner',
- 'type': 'subcontractor',
- 'parent_id': main_partner.id,
- })
-
- # 2. Create a BOM of subcontracting type
- cls.comp1 = cls.env['product.product'].create({
- 'name': 'Component1',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- cls.comp2 = cls.env['product.product'].create({
- 'name': 'Component2',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- cls.finished = cls.env['product.product'].create({
- 'name': 'finished',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- bom_form = Form(cls.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.product_tmpl_id = cls.finished.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp1
- bom_line.product_qty = 1
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp2
- bom_line.product_qty = 1
- cls.bom = bom_form.save()
-
- # Create a BoM for cls.comp2
- cls.comp2comp = cls.env['product.product'].create({
- 'name': 'component for Component2',
- 'type': 'product',
- 'categ_id': cls.env.ref('product.product_category_all').id,
- })
- bom_form = Form(cls.env['mrp.bom'])
- bom_form.product_tmpl_id = cls.comp2.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = cls.comp2comp
- bom_line.product_qty = 1
- cls.comp2_bom = bom_form.save()
-
- def test_flow_1(self):
- """ Don't tick any route on the components and trigger the creation of the subcontracting
- manufacturing order through a receipt picking. Create a reordering rule in the
- subcontracting locations for a component and run the scheduler to resupply. Checks if the
- resupplying actually works
- """
- # Create a receipt picking from the subcontractor
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
-
- # Nothing should be tracked
- self.assertFalse(picking_receipt.display_action_record_components)
- self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
-
- # Check the created manufacturing order
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
- self.assertEqual(len(mo), 1)
- self.assertEqual(len(mo.picking_ids), 0)
- wh = picking_receipt.picking_type_id.warehouse_id
- self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
- self.assertFalse(mo.picking_type_id.active)
-
- # Create a RR
- pg1 = self.env['procurement.group'].create({})
- self.env['stock.warehouse.orderpoint'].create({
- 'name': 'xxx',
- 'product_id': self.comp1.id,
- 'product_min_qty': 0,
- 'product_max_qty': 0,
- 'location_id': self.env.user.company_id.subcontracting_location_id.id,
- 'group_id': pg1.id,
- })
-
- # Run the scheduler and check the created picking
- self.env['procurement.group'].run_scheduler()
- picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
- self.assertEqual(len(picking), 1)
- self.assertEquals(picking.picking_type_id, wh.out_type_id)
- picking_receipt.move_lines.quantity_done = 1
- picking_receipt.button_validate()
- self.assertEquals(mo.state, 'done')
-
- # Now that the picking is done, the details stat button should be visible
- self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
-
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
- self.assertEquals(avail_qty_comp1, -1)
- self.assertEquals(avail_qty_comp2, -1)
- self.assertEquals(avail_qty_finished, 1)
-
- def test_flow_2(self):
- """ Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
- the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
- actually works.
- """
- # Tick "resupply subconractor on order"
- resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
-
- # Create a receipt picking from the subcontractor
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
-
- # Nothing should be tracked
- self.assertFalse(picking_receipt.display_action_record_components)
- self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
-
- # Pickings should directly be created
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
- self.assertEqual(len(mo.picking_ids), 1)
- self.assertEquals(mo.state, 'to_close')
- self.assertEqual(len(mo.picking_ids.move_lines), 2)
-
- picking = mo.picking_ids
- wh = picking.picking_type_id.warehouse_id
-
- # The picking should be a delivery order
- self.assertEquals(picking.picking_type_id, wh.out_type_id)
-
- self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
- self.assertFalse(mo.picking_type_id.active)
-
- # No manufacturing order for `self.comp2`
- comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
- self.assertEqual(len(comp2mo), 0)
-
- picking_receipt.move_lines.quantity_done = 1
- picking_receipt.button_validate()
- self.assertEquals(mo.state, 'done')
-
- # Now that the picking is done, the details stat button should be visible
- self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
-
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
- self.assertEquals(avail_qty_comp1, -1)
- self.assertEquals(avail_qty_comp2, -1)
- self.assertEquals(avail_qty_finished, 1)
-
- def test_flow_3(self):
- """ Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
- creation of the subcontracting manufacturing order through a receipt picking. Checks if the
- resupplying actually works. One of the component has also "manufacture" set and a BOM
- linked. Checks that an MO is created for this one.
- """
- # Tick "resupply subconractor on order"
- resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
-
- # Tick "manufacture" and MTO on self.comp2
- mto_route = self.env['stock.location.route'].search([('name', '=', 'Replenish on Order (MTO)')])
- manufacture_route = self.env['stock.location.route'].search([('name', '=', 'Manufacture')])
- self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
- self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
-
- # Create a receipt picking from the subcontractor
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
-
- # Nothing should be tracked
- self.assertFalse(picking_receipt.display_action_record_components)
- self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
-
- # Pickings should directly be created
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
- self.assertEqual(len(mo.picking_ids), 1)
- self.assertEquals(mo.state, 'to_close')
- self.assertEqual(len(mo.picking_ids.move_lines), 2)
-
- # The picking should be a delivery order
- wh = picking_receipt.picking_type_id.warehouse_id
- self.assertEquals(mo.picking_ids.picking_type_id, wh.out_type_id)
-
- self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
- self.assertFalse(mo.picking_type_id.active)
-
- # As well as a manufacturing order for `self.comp2`
- comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
- self.assertEqual(len(comp2mo), 1)
- picking_receipt.move_lines.quantity_done = 1
- picking_receipt.button_validate()
- self.assertEquals(mo.state, 'done')
-
- # Now that the picking is done, the details stat button should be visible
- self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
-
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
- self.assertEquals(avail_qty_comp1, -1)
- self.assertEquals(avail_qty_comp2, -1)
- self.assertEquals(avail_qty_finished, 1)
-
- def test_flow_4(self):
- """ Check that the correct BoM is chosen accordingly to the partner
- """
- # We create a second partner of type subcontractor
- main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
- subcontractor_partner2 = self.env['res.partner'].create({
- 'name': 'subcontractor_partner',
- 'type': 'subcontractor',
- 'parent_id': main_partner_2.id,
- })
-
- # We create a different BoM for the same product
- comp3 = self.env['product.product'].create({
- 'name': 'Component1',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- })
-
- bom_form = Form(self.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.product_tmpl_id = self.finished.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.comp1
- bom_line.product_qty = 1
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = comp3
- bom_line.product_qty = 1
- bom2 = bom_form.save()
-
- # We assign the second BoM to the new partner
- self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
- bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
-
- # Create a receipt picking from the subcontractor1
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt1 = picking_form.save()
- picking_receipt1.action_confirm()
-
- # Create a receipt picking from the subcontractor2
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = subcontractor_partner2
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished
- move.product_uom_qty = 1
- picking_receipt2 = picking_form.save()
- picking_receipt2.action_confirm()
-
- mo_pick1 = picking_receipt1.move_lines.mapped('move_orig_ids.production_id')
- mo_pick2 = picking_receipt2.move_lines.mapped('move_orig_ids.production_id')
- self.assertEquals(len(mo_pick1), 1)
- self.assertEquals(len(mo_pick2), 1)
- self.assertEquals(mo_pick1.bom_id, self.bom)
- self.assertEquals(mo_pick2.bom_id, bom2)
-
-
-class TestSubcontractingTracking(TransactionCase):
- def setUp(self):
- super(TestSubcontractingTracking, self).setUp()
- # 1: Create a subcontracting partner
- main_company_1 = self.env['res.partner'].create({'name': 'main_partner'})
- self.subcontractor_partner1 = self.env['res.partner'].create({
- 'name': 'Subcontractor 1',
- 'type': 'subcontractor',
- 'parent_id': main_company_1.id,
- })
-
- # 2. Create a BOM of subcontracting type
- # 2.1. Comp1 has tracking by lot
- self.comp1_sn = self.env['product.product'].create({
- 'name': 'Component1',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- 'tracking': 'serial'
- })
- self.comp2 = self.env['product.product'].create({
- 'name': 'Component2',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- })
-
- # 2.2. Finished prodcut has tracking by serial number
- self.finished_lot = self.env['product.product'].create({
- 'name': 'finished',
- 'type': 'product',
- 'categ_id': self.env.ref('product.product_category_all').id,
- 'tracking': 'lot'
- })
- bom_form = Form(self.env['mrp.bom'])
- bom_form.type = 'subcontract'
- bom_form.product_tmpl_id = self.finished_lot.product_tmpl_id
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.comp1_sn
- bom_line.product_qty = 1
- with bom_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.comp2
- bom_line.product_qty = 1
- self.bom_tracked = bom_form.save()
-
- def test_flow_tracked_1(self):
- """ This test mimics test_flow_1 but with a BoM that has tracking included in it.
- """
- # Create a receipt picking from the subcontractor
- picking_form = Form(self.env['stock.picking'])
- picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
- picking_form.partner_id = self.subcontractor_partner1
- with picking_form.move_ids_without_package.new() as move:
- move.product_id = self.finished_lot
- move.product_uom_qty = 1
- picking_receipt = picking_form.save()
- picking_receipt.action_confirm()
-
- # We should be able to call the 'record_components' button
- self.assertTrue(picking_receipt.display_action_record_components)
- self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
-
- # Check the created manufacturing order
- mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
- self.assertEqual(len(mo), 1)
- self.assertEquals(mo.state, 'confirmed')
- self.assertEqual(len(mo.picking_ids), 0)
- wh = picking_receipt.picking_type_id.warehouse_id
- self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
- self.assertFalse(mo.picking_type_id.active)
-
- # Create a RR
- pg1 = self.env['procurement.group'].create({})
- self.env['stock.warehouse.orderpoint'].create({
- 'name': 'xxx',
- 'product_id': self.comp1_sn.id,
- 'product_min_qty': 0,
- 'product_max_qty': 0,
- 'location_id': self.env.user.company_id.subcontracting_location_id.id,
- 'group_id': pg1.id,
- })
-
- # Run the scheduler and check the created picking
- self.env['procurement.group'].run_scheduler()
- picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
- self.assertEqual(len(picking), 1)
- self.assertEquals(picking.picking_type_id, wh.out_type_id)
-
- lot_id = self.env['stock.production.lot'].create({
- 'name': 'lot1',
- 'product_id': self.finished_lot.id
- })
- serial_id = self.env['stock.production.lot'].create({
- 'name': 'lot1',
- 'product_id': self.comp1_sn.id
- })
- produce_form = Form(self.env['mrp.product.produce'].with_context({
- 'active_id': mo.id,
- 'active_ids': [mo.id],
- }))
- produce_form.finished_lot_id = lot_id
- produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id
- wiz_produce = produce_form.save()
- wiz_produce.do_produce()
-
- # We should not be able to call the 'record_components' button
- self.assertFalse(picking_receipt.display_action_record_components)
- # We should see the move lines stat button
- self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
-
- picking_receipt.move_lines.quantity_done = 1
- picking_receipt.move_lines.move_line_ids.lot_name = 'lot00001test'
- picking_receipt.button_validate()
- self.assertEquals(mo.state, 'done')
-
- # Available quantities should be negative at the subcontracting location for each components
- avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
- avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_lot, wh.lot_stock_id)
- self.assertEquals(avail_qty_comp1, -1)
- self.assertEquals(avail_qty_comp2, -1)
- self.assertEquals(avail_qty_finished, 1)
diff --git a/addons/mrp_subcontracting/views/mrp_bom_views.xml b/addons/mrp_subcontracting/views/mrp_bom_views.xml
deleted file mode 100644
index d384f1ab4ea..00000000000
--- a/addons/mrp_subcontracting/views/mrp_bom_views.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- mrp.bom.form.view
- mrp.bom
-
-
-
-
-
-
-
-
-
diff --git a/addons/mrp_subcontracting/views/stock_picking_views.xml b/addons/mrp_subcontracting/views/stock_picking_views.xml
deleted file mode 100644
index 23145eeabbf..00000000000
--- a/addons/mrp_subcontracting/views/stock_picking_views.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- stock.picking.form.view
- stock.picking
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mrp_subcontracting/views/stock_warehouse_views.xml b/addons/mrp_subcontracting/views/stock_warehouse_views.xml
deleted file mode 100644
index a2428f64246..00000000000
--- a/addons/mrp_subcontracting/views/stock_warehouse_views.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- Stock Warehouse Inherit Subcontracting
- stock.warehouse
-
-
-
-
-
-
-
-
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index 9eea5737022..e477b10ba94 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -138,7 +138,7 @@