diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 7c17d62d7ab..5a18c1b9691 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1391,6 +1391,8 @@ class AccountInvoice(models.Model): values[name] = line[name] elif name == 'invoice_line_tax_ids': values[name] = [(6, 0, line[name].ids)] + elif name == 'analytic_tag_ids': + values[name] = [(6, 0, line[name].ids)] result.append((0, 0, values)) return result diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7188c17499b..bb8a29e0fb1 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1531,15 +1531,15 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, }) - if newly_created_move: - if move_date > (self.company_id.period_lock_date or '0000-00-00') and newly_created_move.date != move_date: - # The move date should be the maximum date between payment and invoice (in case - # of payment in advance). However, we should make sure the move date is not - # recorded before the period lock date as the tax statement for this period is - # probably already sent to the estate. - newly_created_move.write({'date': move_date}) - # post move - newly_created_move.post() + if newly_created_move: + if move_date > (self.company_id.period_lock_date or '0000-00-00') and newly_created_move.date != move_date: + # The move date should be the maximum date between payment and invoice (in case + # of payment in advance). However, we should make sure the move date is not + # recorded before the period lock date as the tax statement for this period is + # probably already sent to the estate. + newly_created_move.write({'date': move_date}) + # post move + newly_created_move.post() def _create_tax_basis_move(self): # Check if company_journal for cash basis is set if not, raise exception diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 991ec6451af..130f796c68c 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -291,6 +291,7 @@ class account_register_payments(models.TransientModel): 'partner_id': invoices[0].commercial_partner_id.id, 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], 'partner_bank_account_id': self.partner_bank_account_id.id, + 'multi': False, } @api.multi diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index 35a423b1550..fc4a7f732ea 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -77,7 +77,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): partner_ids = [partner['partner_id'] for partner in partners if partner['partner_id']] lines = dict((partner['partner_id'] or False, []) for partner in partners) if not partner_ids: - return [], [], [] + return [], [], {} # Use one query per period and store results in history (a list variable) # Each history will contain: history[1] = {'': } diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 87bd379879d..f42943f6027 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -130,6 +130,20 @@ var StatementModel = BasicModel.extend({ var line = this.getLine(handle); var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id})); this._addProposition(line, prop); + + // Check whether we have some propositions left + // If we don't, it means we are on an empty page + // so let's go back to the previous page + // Through the offset + var propLineIds = _.map(line.reconciliation_proposition, function(prop) { + return prop.id; + }); + var leftOversProps = _.filter(line.mv_lines, function(mv_line) { + return propLineIds.indexOf(mv_line.id) === -1; + }); + if (line.offset && !leftOversProps.length) { + line.offset -= line.limitMoveLines; + } return $.when(this._computeLine(line), this._performMoveLine(handle)); }, /** @@ -665,7 +679,7 @@ var StatementModel = BasicModel.extend({ return !isNaN(prop.id) && prop.already_paid; }), 'id'), "new_aml_dicts": _.map(_.filter(props, function (prop) { - return isNaN(prop.id); + return isNaN(prop.id) && prop.display; }), self._formatToProcessReconciliation.bind(self, line)), }; @@ -1262,7 +1276,7 @@ var ManualModel = StatementModel.extend({ }); } else { var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id'); - var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation.bind(self, line)); + var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line)); process_reconciliations.push({ id: null, type: null, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index a66b02fa853..ef1a91a8c74 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -423,7 +423,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { // mv_lines var $mv_lines = this.$('.match table tbody').empty(); - _.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) { + var stateMvLines = state.mv_lines || []; + _.each(stateMvLines.slice(0, state.limitMoveLines), function (line) { var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); if (!isNaN(line.id)) { $('') @@ -432,9 +433,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { } $mv_lines.append($line); }); - this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines); + this.$('.match .fa-chevron-right').toggleClass('disabled', stateMvLines.length <= state.limitMoveLines); this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset); - this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : ''); + this.$('.match').css('max-height', !stateMvLines.length && !state.filter.length ? '0px' : ''); // balance this.$('.popover').remove(); diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index e217d381642..d83eb280c33 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -1720,5 +1720,165 @@ QUnit.module('account', { clientAction.destroy(); }); + + QUnit.test('Manual Reconciliation: remove a prop to attain balance and reconcile', function (assert) { + assert.expect(5); + + // tweak the data to fit our needs + this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,null]']); + this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [ + {account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16", + total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait", + total_amount_currency_str: "", id: 999, credit: 0.0, journal_id: [1, "Customer Invoices"], amount_str: "$ 500.00", debit: 500.0, + account_code: "101000", ref: "", already_paid: false} + ]; + + var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options); + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + mockRPC: function (route, args) { + if (args.method === 'process_move_lines') { + assert.deepEqual(args.args, + [ + [{id: null, type: null, + mv_line_ids: [399, 402], + new_mv_line_dicts: []} + ] + ], "should call process_move_lines without the new mv line dict"); + } + + return this._super(route, args); + }, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + }); + + clientAction.appendTo($('#qunit-fixture')); + + // The first reconciliation "line" is where it happens + var widget = clientAction.widgets[0]; + + // Add first prop + widget.$('.match .cell_account_code:first').trigger('click'); + assert.notOk( widget.$('.cell_right .line_info_button').length, + "should not display the partial reconciliation alert"); + + // Add second prop + widget.$('.match .cell_account_code:first').trigger('click'); + + // Check that a create form is here + var writeOffCreate = widget.$('div.create'); + + assert.equal(writeOffCreate.length, 1, + 'A write-off creation should be present'); + + assert.equal(writeOffCreate.find('input[name=amount]').val(), 500, + 'The right amount should be proposed for the write-off'); + + // remove the first line, the other two will balance one another + widget.$('tr[data-line-id="999"] td:first').click() + + var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)'); + assert.equal($buttonReconcile.length, 1, + 'The reconcile button must be visible'); + + $buttonReconcile.click(); + + clientAction.destroy(); + }); + + QUnit.test('Manual Reconciliation: No lines for account', function (assert) { + assert.expect(2); + + var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options); + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + }); + + clientAction.appendTo($('#qunit-fixture')); + + // The second reconciliation "line" is where it happens + var widget = clientAction.widgets[1]; + + var emptyLine = widget.$('tr.mv_line'); + + assert.notOk('data-line-id' in emptyLine.getAttributes(), + 'Empty line should be empty'); + + emptyLine.find('td:first').click(); + + // Check that a create form is here + var writeOffCreate = widget.$('div.create .create_account_id'); + + assert.equal(writeOffCreate.length, 1, + 'A write-off creation should be present'); + + clientAction.destroy(); + }); + + QUnit.test('Automatic Reconciliation: Don\'t loose pager when adding last line of a page', function (assert) { + assert.expect(2); + + function standard_partner (list, partner_id) { + _.each(list, function (item) { + item.partner_id = partner_id; + }); + }; + + this.params.options.params.limitMoveLines = 1; + standard_partner(this.params.mv_lines['[5,"",0,6]'], 8); + + // Overlap of those requests is normal + // the pager is implemented as: + // if there is at least 1 more line than my limit, the pager is active + this.params.mv_lines['[5,"",0,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(0,2); + this.params.mv_lines['[5,"",1,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(1,2); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + var $nextPage = widget.$('.match .match_controls .fa-chevron-right:not(disabled)'); + assert.ok($nextPage.length, + 'We should have a next page'); + $nextPage.click(); + + // Add the second's page record + widget.$('.match tr[data-line-id=112] td:first').click(); + + assert.ok(widget.$('.match tr[data-line-id=109]').length, + 'the record of the first page must be proposed'); + + clientAction.destroy(); + }); }); }); diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 2a4f0f9c12a..12433da1330 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -312,7 +312,7 @@ - + @@ -356,6 +356,18 @@ + + company.res.partner.bank.form + res.partner.bank + + primary + + + 1 + + + + account.bank.journal.tree account.journal @@ -404,7 +416,7 @@ - + diff --git a/addons/account/wizard/setup_wizards.py b/addons/account/wizard/setup_wizards.py index e41e6ac4f1a..407a4b45e27 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -48,6 +48,7 @@ class SetupBarBankConfigWizard(models.TransientModel): _inherits = {'res.partner.bank': 'res_partner_bank_id'} _name = 'account.setup.bank.manual.config' + res_partner_bank_id = fields.Many2one(comodel_name='res.partner.bank', ondelete='cascade', required=True) create_or_link_option = fields.Selection(selection=[('new', 'Create new journal'), ('link', 'Link to an existing journal')], default='new') new_journal_name = fields.Char(compute='compute_new_journal_related_data', inverse='set_linked_journal_id', required=True, help='Will be used to name the Journal related to this bank account') linked_journal_id = fields.Many2one(string="Journal", comodel_name='account.journal', compute='compute_linked_journal_id', inverse='set_linked_journal_id') diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py index 1ca0178ba20..5d3ee07e342 100644 --- a/addons/base_address_extended/models/base_address_extended.py +++ b/addons/base_address_extended/models/base_address_extended.py @@ -76,6 +76,47 @@ class Partner(models.Model): street_value += street_format[previous_pos:] partner.street = street_value + def _split_street_with_params(self, street_raw, street_format): + street_fields = self.get_street_fields() + vals = {} + previous_pos = 0 + field_name = None + # iter on fields in street_format, detected as '%()s' + for re_match in re.finditer(r'%\(\w+\)s', street_format): + field_pos = re_match.start() + if not field_name: + #first iteration: remove the heading chars + street_raw = street_raw[field_pos:] + + # get the substring between 2 fields, to be used as separator + separator = street_format[previous_pos:field_pos] + field_value = None + if separator and field_name: + #maxsplit set to 1 to unpack only the first element and let the rest untouched + tmp = street_raw.split(separator, 1) + if len(tmp) == 2: + field_value, street_raw = tmp + vals[field_name] = field_value + if field_value or not field_name: + # select next field to find (first pass OR field found) + # [2:-2] is used to remove the extra chars '%(' and ')s' + field_name = re_match.group()[2:-2] + else: + # value not found: keep looking for the same field + pass + if field_name not in street_fields: + raise UserError(_("Unrecognized field %s in street format.") % field_name) + previous_pos = re_match.end() + + # last field value is what remains in street_raw minus trailing chars in street_format + trailing_chars = street_format[previous_pos:] + if trailing_chars and street_raw.endswith(trailing_chars): + vals[field_name] = street_raw[:-len(trailing_chars)] + else: + vals[field_name] = street_raw + return vals + + @api.multi @api.depends('street') def _split_street(self): @@ -90,47 +131,18 @@ class Partner(models.Model): street_format = (partner.country_id.street_format or '%(street_number)s/%(street_number2)s %(street_name)s') - vals = {} - previous_pos = 0 street_raw = partner.street - field_name = None - # iter on fields in street_format, detected as '%()s' - for re_match in re.finditer(r'%\(\w+\)s', street_format): - field_pos = re_match.start() - if not field_name: - #first iteration: remove the heading chars - street_raw = street_raw[field_pos:] - - # get the substring between 2 fields, to be used as separator - separator = street_format[previous_pos:field_pos] - field_value = None - if separator and field_name: - #maxsplit set to 1 to unpack only the first element and let the rest untouched - tmp = street_raw.split(separator, 1) - if len(tmp) == 2: - field_value, street_raw = tmp - vals[field_name] = field_value - if field_value or not field_name: - # select next field to find (first pass OR field found) - # [2:-2] is used to remove the extra chars '%(' and ')s' - field_name = re_match.group()[2:-2] - else: - # value not found: keep looking for the same field - pass - if field_name not in street_fields: - raise UserError(_("Unrecognized field %s in street format.") % field_name) - previous_pos = re_match.end() - - # last field value is what remains in street_raw minus trailing chars in street_format - trailing_chars = street_format[previous_pos:] - if trailing_chars and street_raw.endswith(trailing_chars): - vals[field_name] = street_raw[:-len(trailing_chars)] - else: - vals[field_name] = street_raw + vals = self._split_street_with_params(street_raw, street_format) # assign the values to the fields for k, v in vals.items(): partner[k] = v + def write(self, vals): + res = super(Partner, self).write(vals) + if 'country_id' in vals: + self._set_street() + return res + class Company(models.Model): _inherit = 'res.company' diff --git a/addons/base_address_extended/tests/test_street_fields.py b/addons/base_address_extended/tests/test_street_fields.py index d5817070ffd..cf5598b624f 100644 --- a/addons/base_address_extended/tests/test_street_fields.py +++ b/addons/base_address_extended/tests/test_street_fields.py @@ -51,5 +51,5 @@ class TestStreetFields(TransactionCase): self.write_and_assert(p1, {'street': 'Chaussee de Namur'}, 'Chaussee de Namur', 'Chaussee de Namur', '', '') self.write_and_assert(p1, {'street_name': 'Chee de Namur', 'street_number': '40'}, 'Chee de Namur, 40', 'Chee de Namur', '40', '') self.write_and_assert(p1, {'street_number2': '4'}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') - #we don't recompute the street fields when we change the country - self.write_and_assert(p1, {'country_id': self.env.ref('base.us').id}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') + #we do recompute the street fields when we change the country + self.write_and_assert(p1, {'country_id': self.env.ref('base.us').id}, '40/4 Chee de Namur', 'Chee de Namur', '40', '4') diff --git a/addons/base_vat_autocomplete/models/res_partner.py b/addons/base_vat_autocomplete/models/res_partner.py index a143ed67568..630d0ce23db 100644 --- a/addons/base_vat_autocomplete/models/res_partner.py +++ b/addons/base_vat_autocomplete/models/res_partner.py @@ -72,8 +72,8 @@ class ResPartner(models.Model): if len(lines) == 1: lines = [x.strip() for x in lines[0].split(' ') if x] - partner_address['street'] = lines.pop(0) - #_set_address_field(partner, 'street', lines.pop(0)) + vals = self._split_street_with_params(', '.join(lines.pop(0).rsplit(' ', 1)), '%(street_name)s, %(street_number)s/%(street_number2)s') + partner_address.update(vals) if len(lines) > 0: res = _check_city(lines, result['countryCode']) diff --git a/addons/event/data/email_template_data.xml b/addons/event/data/email_template_data.xml index 771e840d63a..48cc3bcf7ec 100644 --- a/addons/event/data/email_template_data.xml +++ b/addons/event/data/email_template_data.xml @@ -372,7 +372,6 @@ Add this event to your calendar Google - Outlook Yahoo diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 8cf1304a110..8902bbcaa49 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -459,6 +459,17 @@ class EventRegistration(models.Model): pass return recipients + @api.multi + def message_get_default_recipients(self): + # Prioritize registration email over partner_id, which may be shared when a single + # partner booked multiple seats + return { + r.id: {'partner_ids': [], + 'email_to': r.email, + 'email_cc': False} + for r in self + } + def _message_post_after_hook(self, message, *args, **kwargs): if self.email and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index c8959775c77..523f2b5a695 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -88,12 +88,12 @@ class HolidaysRequest(models.Model): manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True) notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) number_of_days_temp = fields.Float( - 'Allocation', copy=False, readonly=True, + 'Duration (Days)', copy=False, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Number of days of the leave request according to your working schedule.') number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') number_of_hours = fields.Float( - 'Hours Allocation', copy=False, readonly=True, compute='_compute_number_of_hours', + 'Duration (Hours)', copy=False, readonly=True, compute='_compute_number_of_hours', help='Number of hours of the leave request according to your working schedule.') meeting_id = fields.Many2one('calendar.event', string='Meeting') diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index fabb77751cb..1f5274dda95 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -77,11 +77,11 @@ class HolidaysAllocation(models.Model): states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange') notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) number_of_days_temp = fields.Float( - 'Allocation', copy=False, readonly=True, + 'Duration (days)', copy=False, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Number of days of the leave request according to your working schedule.') number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') - number_of_hours = fields.Float('Number of Hours', help="Number of hours of the leave allocation according to your working schedule.") + number_of_hours = fields.Float('Duration (hours)', help="Number of hours of the leave allocation according to your working schedule.") parent_id = fields.Many2one('hr.leave.allocation', string='Parent') linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests') department_id = fields.Many2one('hr.department', string='Department', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) @@ -258,9 +258,9 @@ class HolidaysAllocation(models.Model): else: self.unit_per_interval = 'days' else: - self.interval_number = 0 + self.interval_number = 1 self.interval_unit = 'weeks' - self.number_per_interval = 0 + self.number_per_interval = 1 self.unit_per_interval = 'hours' #################################################### diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index 620ef641a08..25157ee5684 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -75,16 +75,8 @@ - @@ -92,12 +84,6 @@ - - diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 25319d1f04d..7b78b96b99a 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -97,42 +97,34 @@

+ -