diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 976f6106b2f..32e90d115eb 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -327,6 +327,15 @@ class AccountJournal(models.Model): if 'bank_acc_number' in vals: for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id): journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id')) + # create the relevant refund sequence + if vals.get('refund_sequence'): + for journal in self.filtered(lambda j: j.type in ('sale', 'purchase') and not j.refund_sequence_id): + journal_vals = { + 'name': journal.name, + 'company_id': journal.company_id.id, + 'code': journal.code + } + journal.refund_sequence_id = self.sudo()._create_sequence(journal_vals, refund=True).id return result @@ -342,7 +351,7 @@ class AccountJournal(models.Model): """ Create new no_gap entry sequence for every new Journal""" prefix = self._get_sequence_prefix(vals['code'], refund) seq = { - 'name': vals['name'], + 'name': refund and vals['name'] + _(': Refund') or vals['name'], 'implementation': 'no_gap', 'prefix': prefix, 'padding': 4, diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 2b3b9a83e60..0915e4c9f90 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -560,7 +560,7 @@ class AccountBankStatementLine(models.Model): domain_reconciliation = ['&', '&', ('statement_line_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts), ('payment_id','<>', False)] # Black lines = unreconciled & (not linked to a payment or open balance created by statement - domain_matching = ['&', ('reconciled', '=', False), '|', ('payment_id','=',False), ('statement_id', '<>', False)] + domain_matching = [('reconciled', '=', False)] if self.partner_id.id or overlook_partner: domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]]) else: diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 4503e6ce96f..50a6102b0c8 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -312,7 +312,7 @@ class AccountInvoice(models.Model): default=lambda self: self.env.user) fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position', readonly=True, states={'draft': [('readonly', False)]}) - commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', + commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True, related='partner_id.commercial_partner_id', store=True, readonly=True, help="The commercial entity that will be used on Journal Entries for this invoice") diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 3498b71fe7f..d45f61fe50d 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -307,6 +307,8 @@ class account_journal(models.Model): [action] = self.env.ref('account.%s' % action_name).read() action['context'] = ctx action['domain'] = self._context.get('use_domain', []) + action['views'] = False + action['view_id'] = False return action @api.multi diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 6d49b37728e..a87e906c074 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError from datetime import timedelta @@ -79,6 +80,14 @@ Best Regards,''') @api.multi def write(self, values): + #restrict the closing of FY if there are still unposted entries + if values.get('fiscalyear_lock_date'): + nb_draft_entries = self.env['account.move'].search([ + ('company_id', 'in', [c.id for c in self]), + ('state', '=', 'draft'), + ('date', '<=', values['fiscalyear_lock_date'])]) + if nb_draft_entries: + raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.')) # Reflect the change on accounts for company in self: digits = values.get('accounts_code_digits') or company.accounts_code_digits diff --git a/addons/account/tests/test_account_move_closed_period.py b/addons/account/tests/test_account_move_closed_period.py index 120b7cbb556..f0b700247f8 100644 --- a/addons/account/tests/test_account_move_closed_period.py +++ b/addons/account/tests/test_account_move_closed_period.py @@ -10,10 +10,15 @@ class TestPeriodState(AccountingTestCase): def setUp(self): super(TestPeriodState, self).setUp() + self.user_id = self.env.user self.day_before_yesterday = datetime.now() - timedelta(2) self.yesterday = datetime.now() - timedelta(1) - self.user_id = self.env.user - self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT)}) + self.yesterday_str = self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) + #make sure there is no unposted entry + draft_entries = self.env['account.move'].search([('date', '<=', self.yesterday_str), ('state', '=', 'draft')]) + if draft_entries: + draft_entries.post() + self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday_str}) self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0] self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0] diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 8fd2f020003..ab4a3c75587 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -261,7 +261,7 @@ - + diff --git a/addons/account/views/res_config_view.xml b/addons/account/views/res_config_view.xml index fa2074c39a8..b7ee84bbdda 100644 --- a/addons/account/views/res_config_view.xml +++ b/addons/account/views/res_config_view.xml @@ -21,7 +21,6 @@ -

Fiscal Localization

@@ -33,13 +32,15 @@ Taxes, fiscal positions, chart of accounts & legal statements for your country
-
-
-
-
+ +
+
+
+
+
-

Taxes

diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index d6fb00358c9..4d6e3590037 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -80,8 +80,6 @@ class AccountInvoiceLine(models.Model): @api.onchange('asset_category_id') def onchange_asset_category_id(self): - if not self.asset_category_id: - self.account_id = self.get_invoice_line_account(self.invoice_id.type, self.product_id, self.invoice_id.fiscal_position_id, self.invoice_id.company_id) if self.invoice_id.type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.invoice_id.type == 'in_invoice' and self.asset_category_id: diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 8ee9a40a11f..13b9e39cb22 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -123,7 +123,6 @@ class AccountPayment(models.Model): } else: self.filtered(lambda r: r.state == 'draft').post() - self.write({'state': 'sent'}) return self.do_print_checks() @api.multi diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 804642ac8c2..0ddca4cc626 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -65,7 +65,9 @@ class AuthSignupHome(Home): except Exception, e: qcontext['error'] = e.message or e.name - return request.render('auth_signup.reset_password', qcontext) + response = request.render('auth_signup.reset_password', qcontext) + response.headers['X-Frame-Options'] = 'DENY' + return response def get_auth_signup_config(self): """retrieve the module config (which features are enabled) for the login page""" diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index 3c5cb289232..0adff5f8e1b 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -113,6 +113,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape" || e.key === "Tab" || + e.key === "Backspace" || e.key === "Delete" || /F\d\d?/.test(e.key)) { return true; } else { diff --git a/addons/barcodes/static/src/js/field_float_scannable.js b/addons/barcodes/static/src/js/field_float_scannable.js index 79022362251..801cf32b5e2 100644 --- a/addons/barcodes/static/src/js/field_float_scannable.js +++ b/addons/barcodes/static/src/js/field_float_scannable.js @@ -23,6 +23,10 @@ var FieldFloatScannable = form_widgets.FieldFloat.extend({ }, simulateKeypress: function (e) { + /* only simulate a keypress if it has been previously prevented */ + if (e.originalEvent.dispatched_by_barcode_reader !== true) { + return; + } var character = String.fromCharCode(e.which); var current_str = e.target.value; var str_before_carret = current_str.substring(0, e.target.selectionStart); diff --git a/addons/base_automation/data/base_automation_demo.xml b/addons/base_automation/data/base_automation_demo.xml index d8bedc07020..246320ecf4a 100644 --- a/addons/base_automation/data/base_automation_demo.xml +++ b/addons/base_automation/data/base_automation_demo.xml @@ -35,6 +35,19 @@ [('customer', '=', True)] + + Base Automation: test recursive rule + + on_write + + code + +record = model.browse(env.context['active_id']) +if 'partner_id' in env.context['old_values'][record.id]: + record.write({'state': 'draft'}) + + + Base Automation: test rule on secondary model @@ -44,4 +57,18 @@ + + + Base Automation: test rule on write check context + + on_write + + code + +record = model.browse(env.context['active_id']) +if 'user_id' in env.context['old_values'][record.id]: + record.write({'is_assigned_to_admin': (record.user_id.id == 1)}) + + + diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py index 24267f72821..af8363b92fa 100644 --- a/addons/base_automation/models/base_automation.py +++ b/addons/base_automation/models/base_automation.py @@ -156,13 +156,18 @@ class BaseAutomation(models.Model): def _process(self, records): """ Process action ``self`` on the ``records`` that have not been done yet. """ - # filter out the records on which self has already been done, then mark - # remaining records as done (to avoid recursive processing) + # filter out the records on which self has already been done action_done = self._context['__action_done'] - records -= action_done.setdefault(self, records.browse()) + records_done = action_done.get(self, records.browse()) + records -= records_done if not records: return - action_done[self] |= records + + # mark the remaining records as done (to avoid recursive processing) + action_done = dict(action_done) + action_done[self] = records_done + records + self = self.with_context(__action_done=action_done) + records = records.with_context(__action_done=action_done) # modify records values = {} diff --git a/addons/base_automation/tests/test_base_automation.py b/addons/base_automation/tests/test_base_automation.py index 9fddfad983d..2bea32ec57c 100644 --- a/addons/base_automation/tests/test_base_automation.py +++ b/addons/base_automation/tests/test_base_automation.py @@ -92,6 +92,30 @@ class base_automation_test(common.TransactionCase): self.assertTrue(lead.customer, "Customer field should updated to True") self.assertEqual(lead.user_id, self.user_demo, "Responsible should be change on write of Lead when Customer becomes True.") + def test_11_recomputed_field(self): + """ + Check that a rule is executed whenever a field is recomputed and the + context contains the target field + """ + partner = self.env.ref('base.res_partner_1') + lead = self.create_lead(state='draft', partner_id=partner.id) + self.assertFalse(lead.deadline, 'There should not be a deadline defined') + # change priority and user; this triggers deadline recomputation, and + # the server action should set the boolean field to True + lead.write({'priority': True, 'user_id': self.user_admin.id}) + self.assertTrue(lead.deadline, 'Deadline should be defined') + self.assertTrue(lead.is_assigned_to_admin, 'Lead should be assigned to admin') + + def test_12_recursive(self): + """ Check that a rule is executed recursively by a secondary change. """ + lead = self.create_lead(state='open') + self.assertEqual(lead.state, 'open') + self.assertEqual(lead.user_id, self.user_admin) + # change partner; this should trigger the rule that modifies the state + partner = self.env.ref('base.res_partner_1') + lead.write({'partner_id': partner.id}) + self.assertEqual(lead.state, 'draft') + def test_20_direct_line(self): """ Check that a rule is executed after creating a line record. diff --git a/addons/base_automation/tests/test_models.py b/addons/base_automation/tests/test_models.py index 36893e37447..fd2255dff6f 100644 --- a/addons/base_automation/tests/test_models.py +++ b/addons/base_automation/tests/test_models.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from dateutil import relativedelta +from odoo import fields, models, api class LeadTest(models.Model): @@ -19,6 +20,17 @@ class LeadTest(models.Model): customer = fields.Boolean(related='partner_id.customer', readonly=True, store=True) line_ids = fields.One2many('base.automation.line.test', 'lead_id') + priority = fields.Boolean() + deadline = fields.Boolean(compute='_compute_deadline', store=True) + is_assigned_to_admin = fields.Boolean(string='Assigned to admin user') + + @api.depends('priority') + def _compute_deadline(self): + for record in self: + if not record.priority: + record.deadline = False + else: + record.deadline = fields.Datetime.from_string(record.create_date) + relativedelta.relativedelta(days=3) class LineTest(models.Model): _name = "base.automation.line.test" diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 2c3fa12fa5e..c543f5ea830 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1066,7 +1066,7 @@ class Meeting(models.Model): if [True for item in new_pile if not item]: continue - result_data.append(meeting.get_search_fields(order_fields, r_date=r_date)) + result_data.append(meeting.get_search_fields(order_fields, r_date=r_start_date)) if order_fields: uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values() diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py index e05428593ae..892c0ffe2aa 100644 --- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py +++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import common +from odoo.addons.calendar.models.calendar import calendar_id2real_id class TestRecurrentEvent(common.TransactionCase): @@ -145,3 +146,26 @@ class TestRecurrentEvent(common.TransactionCase): ('start', '>=', '2017-06-30 08:00:00'), ('name', '=', 'Review code with programmer') ]) self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found without stop filter !") + + def test_recurrent_meeting5(self): + # I create a recurrent event and I check if the virtual_id are correct + self.CalendarEvent.create({ + 'count': 5, + 'start': '2012-04-13 11:00:00', + 'stop': '2012-04-13 12:00:00', + 'duration': 1.0, + 'name': 'Test Meeting', + 'recurrency': True, + 'rrule_type': 'daily' + }) + # I search for the first recurrent meeting + meeting = self.CalendarEvent.with_context({'virtual_id': True}).search([ + ('start', '=', '2012-04-13 11:00:00'), ('stop', '=', '2012-04-13 12:00:00') + ]) + virutal_dates = calendar_id2real_id(meeting.id, with_date=True) + + # virtual_dates are used by the calendar view and I check if the start date for the first virtual event is correct. + self.assertEqual(virutal_dates[1], '2012-04-13 11:00:00', "The virtual event doesn't have the correct start date !") + + # virtual_dates are used by the calendar view and I check if the stop date for the first virtual event is correct. + self.assertEqual(virutal_dates[2], '2012-04-13 12:00:00', "The virtual event doesn't have the correct stop date !") diff --git a/addons/crm/data/crm_data.xml b/addons/crm/data/crm_data.xml index 1f27dcb6256..ddfb2fe40ae 100644 --- a/addons/crm/data/crm_data.xml +++ b/addons/crm/data/crm_data.xml @@ -10,7 +10,7 @@ - info + diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 50b20b94fd8..df134ff6c4b 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1044,6 +1044,15 @@ class Lead(models.Model): view_id = super(Lead, self).get_formview_id() return view_id + @api.multi + def message_get_default_recipients(self): + return { + r.id : {'partner_ids': [], + 'email_to': r.email_from, + 'email_cc': False} + for r in self.sudo() + } + @api.multi def message_get_suggested_recipients(self): recipients = super(Lead, self).message_get_suggested_recipients() diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py index a786f1e2c0b..b3bed2ba043 100644 --- a/addons/crm/models/res_config.py +++ b/addons/crm/models/res_config.py @@ -11,7 +11,8 @@ class CRMSettings(models.TransientModel): help="Odoo will generate an email alias based on the sales channel name") alias_prefix = fields.Char('Default Alias Name for Leads') alias_domain = fields.Char('Alias Domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")) - default_generate_lead_from_alias = fields.Boolean() + # TDE TODO: as it is not linked to ir.values anymore, it should be renamed, but not in stable + default_generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails') group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead') module_crm_voip = fields.Boolean("Asterisk (VoIP)") @@ -40,6 +41,26 @@ class CRMSettings(models.TransientModel): IrValues = IrValues.sudo() IrValues.set_default('sale.config.settings', 'generate_sales_team_alias', self.generate_sales_team_alias) + @api.model + def get_default_default_generate_lead_from_alias(self, fields): + return { + 'default_generate_lead_from_alias': self.env['ir.config_parameter'].sudo().get_param('sale_config_settings.default_generate_lead_from_alias') + } + + @api.multi + def set_default_default_generate_lead_from_alias(self): + self.env['ir.config_parameter'].sudo().set_param('sale_config_settings.default_generate_lead_from_alias', self.default_generate_lead_from_alias) + + @api.onchange('group_use_lead') + def _onchange_group_use_lead(self): + """ Reset alias / leads configuration if leads are not used """ + if not self.group_use_lead: + self.default_generate_lead_from_alias = False + + @api.onchange('default_generate_lead_from_alias') + def _onchange_default_generate_lead_from_alias(self): + self.alias_prefix = 'info' if self.default_generate_lead_from_alias else False + @api.model def get_default_alias_prefix(self, fields): alias = self._find_default_lead_alias_id() diff --git a/addons/crm/views/crm_team_views.xml b/addons/crm/views/crm_team_views.xml index 7b007b8a531..23698ecc36c 100644 --- a/addons/crm/views/crm_team_views.xml +++ b/addons/crm/views/crm_team_views.xml @@ -242,4 +242,8 @@ + + ['|', ('res_model_id', '=', False), ('res_model_id.model', 'in', ['crm.lead', 'res.partner'])] + + diff --git a/addons/crm/views/res_config_views.xml b/addons/crm/views/res_config_views.xml index 69df0c5a712..745b5e2b5ed 100644 --- a/addons/crm/views/res_config_views.xml +++ b/addons/crm/views/res_config_views.xml @@ -20,19 +20,24 @@
-
+
-