diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 96ade1cb3d9..4a2846255d2 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -523,7 +523,10 @@ class AccountJournal(models.Model): @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): args = args or [] - recs = self.search(['|', ('code', operator, name), ('name', operator, name)] + args, limit=limit) + connector = '|' + if operator in expression.NEGATIVE_TERM_OPERATORS: + connector = '&' + recs = self.search([connector, ('code', operator, name), ('name', operator, name)] + args, limit=limit) return recs.name_get() @api.multi diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 6c728362378..90223d85923 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -248,7 +248,7 @@ class AccountBankStatement(models.Model): for aml in st_line.journal_entry_ids: moves |= aml.move_id if moves: - moves.post() + moves.filtered(lambda m: m.state != 'posted').post() statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) statements.link_bank_to_partner() statements.write({'state': 'confirm', 'date_done': time.strftime("%Y-%m-%d %H:%M:%S")}) diff --git a/addons/account/tests/account_test_classes.py b/addons/account/tests/account_test_classes.py index cf37e2493ef..8477f4109d9 100644 --- a/addons/account/tests/account_test_classes.py +++ b/addons/account/tests/account_test_classes.py @@ -28,3 +28,30 @@ class AccountingTestCase(HttpCase): if theorical_lines: raise ValidationError('Remaining theorical line (not found). %s)' % ([(aml[0], aml[1], aml[2]) for aml in theorical_lines])) return True + + def ensure_account_property(self, property_name): + '''Ensure the ir.property targetting an account.account passed as parameter exists. + In case it's not: create it with a random account. This is useful when testing with + partially defined localization (missing stock properties for example) + + :param property_name: The name of the property. + ''' + company_id = self.env.user.company_id + field_id = self.env['ir.model.fields'].search( + [('model', '=', 'product.template'), ('name', '=', property_name)], limit=1) + property_id = self.env['ir.property'].search([ + ('company_id', '=', company_id.id), + ('name', '=', property_name), + ('res_id', '=', None), + ('fields_id', '=', field_id.id)], limit=1) + account_id = self.env['account.account'].search([('company_id', '=', company_id.id)], limit=1) + value_reference = 'account.account,%d' % account_id.id + if property_id and not property_id.value_reference: + property_id.value_reference = value_reference + else: + self.env['ir.property'].create({ + 'name': property_name, + 'company_id': company_id.id, + 'fields_id': field_id.id, + 'value_reference': value_reference, + }) diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py index 0331d2c9734..855b4d8bbd1 100644 --- a/addons/base_geolocalize/models/res_partner.py +++ b/addons/base_geolocalize/models/res_partner.py @@ -9,8 +9,9 @@ from odoo.exceptions import UserError def geo_find(addr): + if not addr: + return None url = 'https://maps.googleapis.com/maps/api/geocode/json' - try: result = requests.get(url, params={'sensor': 'false', 'address': addr}).json() except Exception as e: diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py index 0ebd1144ada..ec94bc22d27 100644 --- a/addons/base_import_module/models/ir_module.py +++ b/addons/base_import_module/models/ir_module.py @@ -100,18 +100,23 @@ class IrModule(models.Model): raise UserError(_("File '%s' exceed maximum allowed file size") % zf.filename) with tempdir() as module_dir: - z.extractall(module_dir) - dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))] - for mod_name in dirs: - module_names.append(mod_name) - try: - # assert mod_name.startswith('theme_') - path = opj(module_dir, mod_name) - self._import_module(mod_name, path, force=force) - success.append(mod_name) - except Exception as e: - _logger.exception('Error while importing module') - errors[mod_name] = exception_to_unicode(e) + import odoo.modules as addons + try: + addons.module.ad_paths.append(module_dir) + z.extractall(module_dir) + dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))] + for mod_name in dirs: + module_names.append(mod_name) + try: + # assert mod_name.startswith('theme_') + path = opj(module_dir, mod_name) + self._import_module(mod_name, path, force=force) + success.append(mod_name) + except Exception as e: + _logger.exception('Error while importing module') + errors[mod_name] = exception_to_unicode(e) + finally: + addons.module.ad_paths.remove(module_dir) r = ["Successfully imported module '%s'" % mod for mod in success] for mod, error in pycompat.items(errors): r.append("Error while importing module '%s': %r" % (mod, error)) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 740f6281e9a..9c8b088aeb1 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -567,15 +567,18 @@ class Meeting(models.Model): if not event_date: event_date = datetime.now() - # Convert the event date to saved timezone (or context tz) as it'll - # define the correct hour/day asked by the user to repeat for recurrence. - event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone - rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True) + if self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule: + rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True) + else: + # Convert the event date to saved timezone (or context tz) as it'll + # define the correct hour/day asked by the user to repeat for recurrence. + event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone + rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={}) recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)]) for meeting in recurring_meetings: rset1._exdate.append(todate(meeting.recurrent_id_date)) - return [d.astimezone(pytz.UTC) for d in rset1] + return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1] @api.multi def _get_recurrency_end_date(self): diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 3f5d4511836..ea09ec486b4 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -247,6 +247,7 @@ class Lead(models.Model): team_id = self.env['crm.team']._get_default_team_id(user_id=user_id) return {'team_id': team_id} + @api.constrains('user_id') @api.onchange('user_id') def _onchange_user_id(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index f48eb8d9835..4a2756d2563 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -226,26 +226,26 @@ class DeliveryCarrier(models.Model): if record.delivery_type == 'fixed': PriceRule.search([('carrier_id', '=', record.id)]).unlink() - line_data = { - 'carrier_id': record.id, - 'variable': 'price', - 'operator': '>=', - } - # Create the delivery price rules - if record.free_if_more_than: - line_data.update({ - 'max_value': record.amount, - 'standard_price': 0.0, - 'list_base_price': 0.0, - }) - PriceRule.create(line_data) - if record.fixed_price is not False: - line_data.update({ - 'max_value': 0.0, - 'standard_price': record.fixed_price, - 'list_base_price': record.fixed_price, - }) - PriceRule.create(line_data) + line_data = { + 'carrier_id': record.id, + 'variable': 'price', + 'operator': '>=', + } + # Create the delivery price rules + if record.free_if_more_than: + line_data.update({ + 'max_value': record.amount, + 'standard_price': 0.0, + 'list_base_price': 0.0, + }) + PriceRule.create(line_data) + if record.fixed_price is not False: + line_data.update({ + 'max_value': 0.0, + 'standard_price': record.fixed_price, + 'list_base_price': record.fixed_price, + }) + PriceRule.create(line_data) return True @api.model diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py index ce3b7d1a827..2061e6be1a0 100644 --- a/addons/hr_holidays/models/hr_holidays.py +++ b/addons/hr_holidays/models/hr_holidays.py @@ -329,9 +329,12 @@ class Holidays(models.Model): res = [] for leave in self: if leave.type == 'remove': - res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) + if self.env.context.get('short_name'): + res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp))) + else: + res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) else: - res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp,leave.employee_id.name))) + res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) return res def _check_state_access_right(self, vals): diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 74704a459f2..d8d07c42fd8 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -7,6 +7,13 @@ from odoo import api, fields, models class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' + @api.model + def default_get(self, field_list): + result = super(AccountAnalyticLine, self).default_get(field_list) + if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'): + result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id + return result + task_id = fields.Many2one('project.task', 'Task') project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)]) diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 7886e39b870..c84663c2985 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -67,7 +67,7 @@ - + diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index a797892a4c2..4f728d48698 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -208,8 +208,11 @@ class MrpProduction(models.Model): @api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done') def _compute_post_visible(self): for order in self: - order.post_visible = any(order.move_raw_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) or \ - any(order.move_finished_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) + if order.product_tmpl_id._is_cost_method_standard(): + order.post_visible = any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids) or \ + any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids) + else: + order.post_visible = any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids) @api.multi @api.depends('workorder_ids.state', 'move_finished_ids') diff --git a/addons/project/static/src/js/project.js b/addons/project/static/src/js/project.js index 5da3151aa68..a1ca1a60302 100644 --- a/addons/project/static/src/js/project.js +++ b/addons/project/static/src/js/project.js @@ -51,19 +51,23 @@ KanbanRecord.include({ } function open_cover_images_dialog(attachment_ids) { - var cover_id = self.record.displayed_image_id.raw_value[0]; + var coverID = self.record.displayed_image_id && self.record.displayed_image_id.raw_value[0]; var $content = $(QWeb.render("project.SetCoverModal", { - cover_id: cover_id, + cover_id: coverID, attachment_ids: attachment_ids, })); var $imgs = $content.find('img'); - var dialog = new Dialog(self, { title: _t("Set a Cover Image"), - buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !cover_id, click: function () { - self._updateRecord({displayed_image_id: $imgs.filter('.o_selected').data('id')}); + buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !coverID, click: function () { + var $img = $imgs.filter('.o_selected'); + var data = { + id: $img.data('id'), + display_name: $img.data('name') + }; + self._updateRecord({displayed_image_id: data}); }}, {text: _t("Remove Cover Image"), close: true, click: function () { - self._updateRecord({displayed_image_id: 0}); + self._updateRecord({displayed_image_id: false}); }}, {text: _t("Discard"), close: true}], $content: $content, }).open(); @@ -75,7 +79,12 @@ KanbanRecord.include({ }); $content.on('dblclick', 'img', function (ev) { - self._updateRecord({displayed_image_id: $(ev.currentTarget).data('id')}); + var $img = $(ev.currentTarget); + var data = { + id: $img.data('id'), + display_name: $img.data('name') + }; + self._updateRecord({displayed_image_id: data}); dialog.close(); }); } diff --git a/addons/project/static/src/xml/project.xml b/addons/project/static/src/xml/project.xml index d9c4f3fa675..a2d222784b9 100644 --- a/addons/project/static/src/xml/project.xml +++ b/addons/project/static/src/xml/project.xml @@ -2,7 +2,7 @@
- +
There is no available image to be set as cover. Send a message on the task with an attached image.
diff --git a/addons/project/static/tests/project_tests.js b/addons/project/static/tests/project_tests.js new file mode 100644 index 00000000000..fa92415684e --- /dev/null +++ b/addons/project/static/tests/project_tests.js @@ -0,0 +1,133 @@ +odoo.define('project.project_kanban_tests', function (require) { +"use strict"; + +var KanbanView = require('web.KanbanView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('project', { + beforeEach: function () { + this.data = { + 'ir.attachment': { + fields: { + name: { + string: "Name", + type: "char" + }, + }, + records: [{ + id: 1, + name: "1.png" + }, + { + id: 2, + name: "2.png" + }, + ] + }, + 'project.task': { + fields: { + name: { + string: "Task Title", + type: "char" + }, + sequence: { + string: "sequence", + type: "integer" + }, + displayed_image_id: { + string: "cover", + type: "many2one", + relation: "ir.attachment" + }, + kanban_state: { + string: "State", + type: "selection", + selection: [ + ["abc", "ABC"], + ["def", "DEF"], + ["ghi", "GHI"] + ] + }, + }, + records: [{ + id: 1, + name: "task1", + sequence: 1, + kanban_state: "abc" + }, + { + id: 2, + name: "task2", + sequence: 2, + kanban_state: "abc" + }, + ] + }, + }; + } +}, function () { + QUnit.module('image test'); + + QUnit.test('cover_image_test', function (assert) { + assert.expect(6); + var kanban = createView({ + View: KanbanView, + model: 'project.task', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '' + + '' + + '
'+ + ''+ + '
'+ + '
' + + '
' + + '
' + + '
', + mockRPC: function(route, args) { + if (args.model === 'ir.attachment' && args.method === 'search_read') { + return $.when([{ + id: 1, + name: "1.png" + },{ + id: 2, + name: "2.png" + }]); + } + if (args.model === 'project.task' && args.method === 'write') { + assert.step(args.args[0][0]); + return this._super(route, args); + } + return this._super(route, args); + }, + }); + assert.strictEqual(kanban.$('img').length, 0, "Initially there is no image."); + kanban.$('.o_dropdown_kanban [data-type=set_cover]').eq(0).click(); + // single click on image + $('.modal').find("img[data-id='1']").click(); + $('.modal-footer .btn-primary').click(); + assert.strictEqual(kanban.$('img[src*="/web/image/1"]').length, 1, "Image inserted in record"); + $('.o_dropdown_kanban [data-type=set_cover]').eq(1).click(); + // double click on image + $('.modal').find("img[data-id='2']").dblclick(); + assert.strictEqual(kanban.$('img[src*="/web/image/2"]').length, 1, "Image inserted after double click"); + // varify write on both kanban record + assert.verifySteps([1,2]); + kanban.destroy(); + }); +}); +}); diff --git a/addons/project/views/project_templates.xml b/addons/project/views/project_templates.xml index 3806674c4b9..8baab3b7657 100644 --- a/addons/project/views/project_templates.xml +++ b/addons/project/views/project_templates.xml @@ -10,4 +10,9 @@
+ diff --git a/addons/project_issue_sheet/views/project_issue_view.xml b/addons/project_issue_sheet/views/project_issue_view.xml index 7bd27cf2c7f..99928d28bdb 100644 --- a/addons/project_issue_sheet/views/project_issue_view.xml +++ b/addons/project_issue_sheet/views/project_issue_view.xml @@ -10,7 +10,7 @@ - + diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 9ec3ce6b9d5..84dbee6a3fd 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -105,6 +105,9 @@ class TestSaleOrder(TestSale): def test_cost_invoicing(self): """ Test confirming a vendor invoice to reinvoice cost on the so """ + # force the pricelist to have the same currency as the company + self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id + serv_cost = self.env.ref('product.service_cost_01') prod_gap = self.env.ref('product.product_product_1') so = self.env['sale.order'].create({ diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py index bc8d314206e..91a094bf901 100644 --- a/addons/sale_expense/tests/test_sale_expense.py +++ b/addons/sale_expense/tests/test_sale_expense.py @@ -7,6 +7,9 @@ from odoo.addons.sale.tests.test_sale_common import TestSale class TestSaleExpense(TestSale): def test_sale_expense(self): """ Test the behaviour of sales orders when managing expenses """ + # force the pricelist to have the same currency as the company + self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id + # create a so with a product invoiced on delivery prod = self.env.ref('product.product_product_1') so = self.env['sale.order'].create({ diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index 64ac5d7a8aa..17ba8ad96e2 100644 --- a/addons/sale_timesheet/models/account_invoice.py +++ b/addons/sale_timesheet/models/account_invoice.py @@ -53,7 +53,7 @@ class AccountInvoice(models.Model): def _compute_timesheet_revenue(self): for invoice in self: - for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service'): + for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)): uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([ ('so_line', 'in', invoice_line.sale_line_ids.ids), ('project_id', '!=', False), @@ -63,13 +63,12 @@ class AccountInvoice(models.Model): # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs) if uninvoiced_timesheet_lines: - precision = invoice_line.currency_id.decimal_places # delivered : update revenue with the prorata of number of hours on the timesheet line if invoice_line.product_id.invoice_policy == 'delivery': - invoiced_price_per_hour = float_round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))), precision) + invoiced_price_per_hour = invoice_line.currency_id.round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount')))) # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys(uninvoiced_timesheet_lines.mapped('company_currency_id').ids, 0.0) - for index, timesheet_line in enumerate(uninvoiced_timesheet_lines): + for index, timesheet_line in enumerate(uninvoiced_timesheet_lines.sorted(key=lambda ts: ts.date)): if index+1 != len(uninvoiced_timesheet_lines): line_revenue = invoice_line.currency_id.compute(invoiced_price_per_hour, timesheet_line.company_currency_id) * timesheet_line.unit_amount total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue @@ -92,7 +91,7 @@ class AccountInvoice(models.Model): # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys(no_zero_timesheet_revenue.mapped('company_currency_id').ids, 0.0) - for index, timesheet_line in enumerate(no_zero_timesheet_revenue): + for index, timesheet_line in enumerate(no_zero_timesheet_revenue.sorted(key=lambda ts: ts.date)): if index+1 != len(no_zero_timesheet_revenue): price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id) price_subtotal_sol = timesheet_line.so_line.currency_id.compute(timesheet_line.so_line.price_subtotal, timesheet_line.company_currency_id) diff --git a/addons/stock/i18n/stock.pot b/addons/stock/i18n/stock.pot index 1296f083384..8e86883b4aa 100644 --- a/addons/stock/i18n/stock.pot +++ b/addons/stock/i18n/stock.pot @@ -6383,6 +6383,12 @@ msgid "You cannot set a negative product quantity in an inventory line:\n" " %s - qty: %s" msgstr "" +#. module: stock +#: code:addons/stock/models/stock_production_lot.py:37 +#, python-format +msgid "You are not allowed to create a lot for this picking type" +msgstr "" + #. module: stock #: code:addons/stock/models/stock_move.py:927 #, python-format diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 54b4a877fd7..6e26cf6dc77 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -419,6 +419,9 @@ class ProductTemplate(models.Model): relation="stock.location.route", string="Category Routes", related='categ_id.total_route_ids') + def _is_cost_method_standard(self): + return True + def _compute_quantities(self): res = self._compute_quantities_dict() for template in self: diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index 60f370fff51..a568cd80fe3 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ +from odoo.exceptions import UserError class ProductionLot(models.Model): @@ -27,6 +28,15 @@ class ProductionLot(models.Model): ('name_ref_uniq', 'unique (name, product_id)', 'The combination of serial number and product must be unique !'), ] + @api.model + def create(self, vals): + pack_id = self.env.context.get('active_pack_operation', False) + if pack_id: + pack = self.env['stock.pack.operation'].browse(pack_id) + if pack.picking_id and not pack.picking_id.picking_type_id.use_create_lots: + raise UserError(_("You are not allowed to create a lot for this picking type")) + return super(ProductionLot, self).create(vals) + @api.one @api.depends('quant_ids.qty') def _product_qty(self): diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index 20de2d25775..8453a49b4e3 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -66,6 +66,12 @@ class StockScrap(models.Model): scrap.do_scrap() return scrap + @api.multi + def unlink(self): + if 'done' in self.mapped('state'): + raise UserError(_('You cannot delete a scrap which is done.')) + return super(StockScrap, self).unlink() + def _get_origin_moves(self): return self.picking_id and self.picking_id.move_lines.filtered(lambda x: x.product_id == self.product_id) diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index dc8f1756134..6b13597199d 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -78,4 +78,8 @@ access_barcode_rule_stock_manager,barcode.rule.stock.manager,barcodes.model_barc access_stock_forecast_user,report.stock.forecast.user,model_report_stock_forecast,stock.group_stock_user,1,0,0,0 access_stock_forecast_manager,report.stock.forecast.manager,model_report_stock_forecast,stock.group_stock_manager,1,1,1,1 access_stock_scrap_user,stock.scrap.user,model_stock_scrap,stock.group_stock_user,1,1,1,0 -access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0 \ No newline at end of file +access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0 +access_product_attribute_manager,product.attribute manager,product.model_product_attribute,stock.group_stock_manager,1,1,1,1 +access_product_attribute_value_manager,product.attribute manager value,product.model_product_attribute_value,stock.group_stock_manager,1,1,1,1 +access_product_attribute_price_manager,product.attribute manager price,product.model_product_attribute_price,stock.group_stock_manager,1,1,1,1 +access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1 \ No newline at end of file diff --git a/addons/stock/views/stock_pack_operation_views.xml b/addons/stock/views/stock_pack_operation_views.xml index 5b6919dab83..cd8469d4672 100644 --- a/addons/stock/views/stock_pack_operation_views.xml +++ b/addons/stock/views/stock_pack_operation_views.xml @@ -85,7 +85,7 @@ + context="{'default_product_id': parent.product_id, 'active_pack_operation': parent.id}"/>