diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 54c4e63e397..dfd0e9fe02d 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -4,7 +4,7 @@ from odoo import api, fields, models, _ from odoo.osv import expression from odoo.tools import float_is_zero, pycompat from odoo.tools import float_compare, float_round, float_repr -from odoo.tools.misc import formatLang +from odoo.tools.misc import formatLang, format_date from odoo.exceptions import UserError, ValidationError import time diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f13341b0826..539c7c9f82a 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -5,7 +5,7 @@ from collections import OrderedDict from odoo import api, fields, models, _ from odoo.osv import expression from odoo.exceptions import RedirectWarning, UserError, ValidationError -from odoo.tools.misc import formatLang +from odoo.tools.misc import formatLang, format_date from odoo.tools import float_is_zero, float_compare from odoo.tools.safe_eval import safe_eval from odoo.addons import decimal_precision as dp diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index b04cae9a322..286392c43ed 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -447,6 +447,7 @@ var StatementModel = BasicModel.extend({ model: 'account.reconciliation.widget', method: 'get_bank_statement_line_data', args: [ids, excluded_ids], + context: self.context, }) .then(self._formatLine.bind(self)); }, @@ -1100,6 +1101,7 @@ var StatementModel = BasicModel.extend({ model: 'account.reconciliation.widget', method: 'get_move_lines_for_bank_statement_line', args: [line.id, line.st_line.partner_id, excluded_ids, filter, offset, limit], + context: this.context, }) .then(this._formatMoveLine.bind(this, handle)); }, @@ -1165,6 +1167,7 @@ var ManualModel = StatementModel.extend({ */ load: function (context) { var self = this; + this.context = context; var domain_account_id = []; if (context && context.company_ids) { @@ -1471,6 +1474,7 @@ var ManualModel = StatementModel.extend({ model: 'account.reconciliation.widget', method: 'get_move_lines_for_manual_reconciliation', args: args, + context: this.context, }) .then(this._formatMoveLine.bind(this, handle)); }, diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 5941db4c480..adc2316cbb1 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -173,7 +173,7 @@ class Lead(models.Model): @api.depends('date_open') def _compute_day_open(self): """ Compute difference between create date and open date """ - for lead in self.filtered(lambda l: l.date_open): + for lead in self.filtered(lambda l: l.date_open and l.create_date): date_create = fields.Datetime.from_string(lead.create_date) date_open = fields.Datetime.from_string(lead.date_open) lead.day_open = abs((date_open - date_create).days) @@ -181,7 +181,7 @@ class Lead(models.Model): @api.depends('date_closed') def _compute_day_close(self): """ Compute difference between current date and log date """ - for lead in self.filtered(lambda l: l.date_closed): + for lead in self.filtered(lambda l: l.date_closed and l.create_date): date_create = fields.Datetime.from_string(lead.create_date) date_close = fields.Datetime.from_string(lead.date_closed) lead.day_close = abs((date_close - date_create).days) @@ -716,7 +716,7 @@ class Lead(models.Model): partner_match_domain.append(('partner_id', '=', partner_id)) partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain if not partner_match_domain: - return [] + return self.env['crm.lead'] domain = partner_match_domain if not include_lost: domain += ['&', ('active', '=', True), ('probability', '<', 100)] diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 23909de6dea..eef295cfdd7 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -720,6 +720,6 @@ class HrExpenseSheet(models.Model): for expense_report in self.filtered(lambda hol: hol.state == 'submit'): self.activity_schedule( 'hr_expense.mail_act_expense_approval', fields.Date.today(), - user_id=expense_report._get_responsible_for_approval().id) + user_id=expense_report.sudo()._get_responsible_for_approval().id) self.filtered(lambda hol: hol.state == 'approve').activity_feedback(['hr_expense.mail_act_expense_approval']) self.filtered(lambda hol: hol.state == 'cancel').activity_unlink(['hr_expense.mail_act_expense_approval']) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 1f5274dda95..9013dc2f518 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -467,12 +467,12 @@ class HolidaysAllocation(models.Model): elif allocation.state == 'confirm': allocation.activity_schedule( 'hr_holidays.mail_act_leave_allocation_approval', fields.Date.today(), - user_id=allocation._get_responsible_for_approval().id) + user_id=allocation.sudo()._get_responsible_for_approval().id) elif allocation.state == 'validate1': allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval']) allocation.activity_schedule( 'hr_holidays.mail_act_leave_allocation_second_approval', fields.Date.today(), - user_id=allocation._get_responsible_for_approval().id) + user_id=allocation.sudo()._get_responsible_for_approval().id) elif allocation.state == 'validate': to_do |= allocation elif allocation.state == 'refuse': diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index c3728fff213..77212a1367c 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -12,8 +12,6 @@ - - diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index a84742a800e..7d8564d0430 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -616,8 +616,8 @@ class Channel(models.Model): AND P.partner_id IN %s AND channel_type LIKE 'chat' GROUP BY P.channel_id - HAVING COUNT(P.partner_id) = %s - """, (tuple(partners_to), len(partners_to),)) + HAVING array_agg(P.partner_id ORDER BY P.partner_id) = %s + """, (tuple(partners_to), sorted(list(partners_to)),)) result = self.env.cr.dictfetchall() if result: # get the existing channel between the given partners diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 502f80b8cae..6ab091e2f43 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1886,6 +1886,7 @@ class MailThread(models.AbstractModel): if not attachment: attachment = fname_mapping.get(node.get('data-filename'), '') if attachment: + attachment.generate_access_token() node.set('src', '/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token)) postprocessed = True if postprocessed: diff --git a/addons/mrp/static/src/scss/mrp.scss b/addons/mrp/static/src/scss/mrp.scss deleted file mode 100644 index a0ce531779f..00000000000 --- a/addons/mrp/static/src/scss/mrp.scss +++ /dev/null @@ -1,8 +0,0 @@ -.o_form_view { - .o_field_pdfviewer { - width: 100%; - height: 450px; - border: 0; - margin-top: 10px; - } -} diff --git a/addons/mrp/views/mrp_templates.xml b/addons/mrp/views/mrp_templates.xml index 2c746900a69..0b486becb0f 100644 --- a/addons/mrp/views/mrp_templates.xml +++ b/addons/mrp/views/mrp_templates.xml @@ -3,7 +3,6 @@ diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 5e04c88c6a7..852f5927c90 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -144,7 +144,7 @@ class PosOrder(models.Model): cash_journal_id = cash_journal[0].id order.add_payment({ 'amount': -pos_order['amount_return'], - 'payment_date': fields.Datetime.now(), + 'payment_date': fields.Date.context_today(self), 'payment_name': _('return'), 'journal': cash_journal_id, }) diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 6edad386082..5a112226bdd 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -419,7 +419,8 @@
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index a303ce1c480..9bc95f95e9f 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -906,11 +906,13 @@ class StockMove(models.Model): assigned_moves = self.env['stock.move'] partially_available_moves = self.env['stock.move'] for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']): + missing_reserved_uom_quantity = move.product_uom_qty - move.reserved_availability + missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP') if move.location_id.should_bypass_reservation()\ or move.product_id.type == 'consu': # create the move line(s) but do not impact quants if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots): - for i in range(0, int(move.product_qty - move.reserved_availability)): + for i in range(0, int(missing_reserved_quantity)): self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1)) else: to_update = move.move_line_ids.filtered(lambda ml: ml.product_uom_id == move.product_uom and @@ -921,16 +923,16 @@ class StockMove(models.Model): not ml.package_id and not ml.owner_id) if to_update: - to_update[0].product_uom_qty += move.product_qty - move.reserved_availability + to_update[0].product_uom_qty += missing_reserved_uom_quantity else: - self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=move.product_qty - move.reserved_availability)) + self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=missing_reserved_quantity)) assigned_moves |= move else: if not move.move_orig_ids: if move.procure_method == 'make_to_order': continue # If we don't need any quantity, consider the move assigned. - need = move.product_qty - move.reserved_availability + need = missing_reserved_quantity if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding): assigned_moves |= move continue diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index bdacc2e3a3e..eaae751f318 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -1099,6 +1099,27 @@ class StockMove(TransactionCase): move_partial._action_assign() self.assertEqual(move_partial.state, 'assigned') + def test_availability_9(self): + """ Test the assignment mechanism when the product quantity is increase + on a receipt move. + """ + move_receipt = self.env['stock.move'].create({ + 'name': 'test_receipt_edit', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_dozen.id, + 'product_uom_qty': 1.0, + }) + + move_receipt._action_confirm() + move_receipt._action_assign() + self.assertEqual(move_receipt.state, 'assigned') + move_receipt.product_uom_qty = 3.0 + move_receipt._action_assign() + self.assertEqual(move_receipt.state, 'assigned') + self.assertEqual(move_receipt.move_line_ids.product_uom_qty, 3) + def test_unreserve_1(self): """ Check that unreserving a stock move sets the products reserved as available and set the state back to confirmed. diff --git a/addons/web/models/ir_qweb.py b/addons/web/models/ir_qweb.py index 29426b9d8a8..e61d5ef2bf7 100644 --- a/addons/web/models/ir_qweb.py +++ b/addons/web/models/ir_qweb.py @@ -65,6 +65,7 @@ class Image(models.AbstractModel): atts["alt"] = alt atts["data-zoom"] = src_zoom and u'1' or None atts["data-zoom-image"] = src_zoom + atts["data-no-post-process"] = options.get('data-no-post-process') atts = self.env['ir.qweb']._post_processing_att('img', atts, options.get('template_options')) diff --git a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js index d0eed176d9c..6fec5795bee 100644 --- a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js +++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js @@ -103,6 +103,14 @@ odoo.define('web.KeyboardNavigationMixin', function (require) { * return {undefined|false} */ _onKeyDown: function (keyDownEvent) { + if ($('body.o_ui_blocked').length && + (keyDownEvent.altKey || keyDownEvent.key === 'Alt') && + !keyDownEvent.ctrlKey) { + if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false; + if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation(); + if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true; + return false; + } if (!this._areAccessKeyVisible && (keyDownEvent.altKey || keyDownEvent.key === 'Alt') && !keyDownEvent.ctrlKey) { diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js index be0f012752c..cd9a73ef22b 100644 --- a/addons/web/static/src/js/core/misc.js +++ b/addons/web/static/src/js/core/misc.js @@ -52,6 +52,29 @@ if ($.blockUI) { } +/** + * Remove the "accesskey" attributes to avoid the use of the access keys + * while the blockUI is enable. + */ + +function blockAccessKeys() { + var elementWithAccessKey = []; + elementWithAccessKey = document.querySelectorAll('[accesskey]'); + _.each(elementWithAccessKey, function (elem) { + elem.setAttribute("data-accesskey",elem.getAttribute('accesskey')); + elem.removeAttribute('accesskey'); + }); +} + +function unblockAccessKeys() { + var elementWithDataAccessKey = []; + elementWithDataAccessKey = document.querySelectorAll('[data-accesskey]'); + _.each(elementWithDataAccessKey, function (elem) { + elem.setAttribute('accesskey', elem.getAttribute('data-accesskey')); + elem.removeAttribute('data-accesskey'); + }); +} + var throbbers = []; function blockUI() { @@ -60,6 +83,7 @@ function blockUI() { throbbers.push(throbber); throbber.appendTo($(".oe_blockui_spin_container")); $(document.body).addClass('o_ui_blocked'); + blockAccessKeys(); return tmp; } @@ -67,6 +91,7 @@ function unblockUI() { _.invoke(throbbers, 'destroy'); throbbers = []; $(document.body).removeClass('o_ui_blocked'); + unblockAccessKeys(); return $.unblockUI.apply($, arguments); } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 4b4a4a86c16..32bab9373a9 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -905,8 +905,14 @@ var BasicModel = AbstractModel.extend({ params: params, }) .then(function () { + var offset = options.offset ? options.offset : 0; + var old_data = data.data.slice(); data.data = _.sortBy(data.data, function (d) { - return _.indexOf(resIDs, self.localData[d].res_id); + if (_.contains(resIDs, self.localData[d].res_id)) { + return _.indexOf(resIDs, self.localData[d].res_id) + offset; + } else { + return _.indexOf(old_data, d); + } }); data.res_ids = []; _.each(data.data, function (d) { diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index 8a5ee6201bd..78d7e30379e 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -407,4 +407,12 @@ } } } + + // PDF Viewer + &.o_field_pdfviewer, .o_pdfview_iframe { + width: 100%; + height: 450px; + border: 0; + margin-top: 10px; + } } diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 5e82766b051..f4fcb213a05 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -877,6 +877,23 @@ var MockServer = Class.extend({ return data; }, + /** + * Simulates a 'resequence' operation + * + * @private + * @param {string} model + * @param {string} field + * @param {Array} ids + */ + _mockResequence: function (args) { + var offset = args.offset ? Number(args.offset) : 0; + var field = args.field ? args.field : 'sequence'; + var records = this.data[args.model].records; + for (var i in args.ids) { + var record = _.findWhere(this.data[args.model].records, {id: args.ids[i]}); + record[field] = Number(i) + offset; + } + }, /** * Simulate a 'search_count' operation * @@ -1036,7 +1053,7 @@ var MockServer = Class.extend({ return $.when(this._mockSearchReadController(args)); case '/web/dataset/resequence': - return $.when(); + return $.when(this._mockResequence(args)); } if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) { return $.when(); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 95f2af74f09..a6b28ed24e7 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3079,20 +3079,118 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('result of consecutive resequences is correctly sorted', function (assert) { + assert.expect(9); + this.data = { // we want the data to be minimal to have a minimal test + foo: { + fields: {int_field: {string: "int_field", type: "integer", sortable: true}}, + records: [ + {id: 1, int_field: 0}, + {id: 2, int_field: 1}, + {id: 3, int_field: 2}, + {id: 4, int_field: 3}, + ] + } + }; + var moves = 0; + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (route === '/web/dataset/resequence') { + if (moves === 0) { + assert.deepEqual(args, { + model: "foo", + ids: [4, 3], + offset: 2, + field: "int_field", + }); + } + if (moves === 1) { + assert.deepEqual(args, { + model: "foo", + ids: [1, 4, 2, 3], + field: "int_field", + }); + } + if (moves === 2) { + assert.deepEqual(args, { + model: "foo", + ids: [2, 4], + offset: 1, + field: "int_field", + }); + } + if (moves === 3) { + assert.deepEqual(args, { + model: "foo", + ids: [1, 4, 2, 3], + field: "int_field", + }); + } + moves += 1; + } + return this._super.apply(this, arguments); + }, + }); + assert.strictEqual(list.$('tbody tr td').text(), '1234', + "default should be sorted by id"); + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').eq(2), + {position: 'top'} + ); + assert.strictEqual(list.$('tbody tr td').text(), '1243', + "the int_field (sequence) should have been correctly updated"); + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(2), + list.$('tbody tr').eq(1), + {position: 'top'} + ); + assert.deepEqual(list.$('tbody tr td').text(), '1423', + "the int_field (sequence) should have been correctly updated"); + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(1), + list.$('tbody tr').eq(3), + {position: 'top'} + ); + assert.deepEqual(list.$('tbody tr td').text(), '1243', + "the int_field (sequence) should have been correctly updated"); + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(2), + list.$('tbody tr').eq(1), + {position: 'top'} + ); + assert.deepEqual(list.$('tbody tr td').text(), '1423', + "the int_field (sequence) should have been correctly updated"); + list.destroy(); + }); + QUnit.test('editable list with handle widget', function (assert) { assert.expect(12); + // 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: '' + + arch: '' + '' + '' + '', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { - assert.strictEqual(args.offset, -4, + assert.strictEqual(args.offset, 1, "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); @@ -3140,6 +3238,12 @@ QUnit.module('Views', { QUnit.test('editable list with handle widget with slow network', function (assert) { assert.expect(15); + // 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 def = $.Deferred(); var list = createView({ @@ -3152,7 +3256,7 @@ QUnit.module('Views', { '', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { - assert.strictEqual(args.offset, -4, + assert.strictEqual(args.offset, 1, "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index ced982eed4a..fd1aa083a3f 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -235,7 +235,7 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage): grade_domain += [('country_id', '=', country.id)] grades = partner_obj.sudo().read_group( grade_domain, ["id", "grade_id"], - groupby="grade_id", orderby="grade_id DESC") + groupby="grade_id") grades_partners = partner_obj.sudo().search_count(grade_domain) # flag active grade for grade_dict in grades: diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 751f4aed9ee..fc2f1a70318 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -280,7 +280,7 @@ class IrHttp(models.AbstractModel): obj = env.ref(xmlid, False) elif id and model == 'ir.attachment' and access_token: obj = env[model].sudo().browse(int(id)) - if not consteq(obj.access_token, access_token): + if not consteq(obj.access_token or '', access_token): return (403, [], None) elif id and model in env.registry: obj = env[model].browse(int(id)) diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index d082db9e197..adcb938a0a3 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -346,7 +346,7 @@ class IrQWeb(models.AbstractModel, QWeb): files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media}) elif can_aggregate(src) and el.tag == 'script': atype = 'text/javascript' - path = [segment for segment in href.split('/') if segment] + path = [segment for segment in src.split('/') if segment] filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) else: