diff --git a/MANIFEST.in b/MANIFEST.in index 46e071c97c8..d4e3af91152 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include README.md recursive-include odoo *.css recursive-include odoo *.csv recursive-include odoo *.doc +recursive-include odoo *.docx recursive-include odoo *.eml recursive-include odoo *.eot recursive-include odoo *.gif @@ -16,6 +17,8 @@ recursive-include odoo *.less recursive-include odoo *.md recursive-include odoo *.mp3 recursive-include odoo *.ogg +recursive-include odoo *.ods +recursive-include odoo *.odt recursive-include odoo *.otf recursive-include odoo *.pdf recursive-include odoo *.png @@ -35,8 +38,10 @@ recursive-include odoo *.wsdl recursive-include odoo *.xls recursive-include odoo *.xsd recursive-include odoo *.xsl +recursive-include odoo *.xlsx recursive-include odoo *.xml recursive-include odoo *.yml +recursive-include odoo *.zip recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt recursive-include odoo/addons/l10n_mx_reports *.xslt recursive-exclude * *.py[co] diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index a1a7754a076..8ead6af8961 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -17,7 +17,6 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'security/ir.model.access.csv', 'data/data_account_type.xml', 'data/account_data.xml', - 'data/ir_cron_data.xml', 'views/account_menuitem.xml', 'views/account_payment_view.xml', 'wizard/account_reconcile_view.xml', diff --git a/addons/account/controllers/mail.py b/addons/account/controllers/mail.py index f30c69e21ba..564856b909c 100644 --- a/addons/account/controllers/mail.py +++ b/addons/account/controllers/mail.py @@ -11,6 +11,7 @@ from odoo.tools.misc import consteq class MailController(MailController): + @classmethod def _redirect_to_record(cls, model, res_id, access_token=None): # If the current user doesn't have access to the invoice, but provided # a valid access token, redirect him to the front-end view. diff --git a/addons/account/data/ir_cron_data.xml b/addons/account/data/ir_cron_data.xml deleted file mode 100644 index 6e3ceb702aa..00000000000 --- a/addons/account/data/ir_cron_data.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Account: generate recurring vendor bills - - code - False - model._cron_generate_recurring_vendor_bill() - 1 - days - -1 - - diff --git a/addons/account/data/payment_receipt_data.xml b/addons/account/data/payment_receipt_data.xml index 3190edfb151..d3c02413732 100644 --- a/addons/account/data/payment_receipt_data.xml +++ b/addons/account/data/payment_receipt_data.xml @@ -14,7 +14,7 @@ ${object.partner_id.lang}

Dear ${object.partner_id.name},

-

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${object.amount} ${object.currency_id.name} from ${object.company_id.name}.

+

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${format_amount(object.amount, object.currency_id)} from ${object.company_id.name}.

If you have any questions, please do not hesitate to contact us.

Best regards, % if user and user.signature: diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 2f5c1421a60..b7d8d52f096 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -441,7 +441,7 @@ class AccountJournal(models.Model): for journal in self: if journal.sequence_id and journal.sequence_number_next: sequence = journal.sequence_id._get_current_sequence() - sequence.number_next = journal.sequence_number_next + sequence.sudo().number_next = journal.sequence_number_next @api.multi # do not depend on 'refund_sequence_id.date_range_ids', because @@ -523,6 +523,8 @@ class AccountJournal(models.Model): if ('company_id' in vals and journal.company_id.id != vals['company_id']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) + if self.bank_account_id: + self.bank_account_id.company_id = vals['company_id'] if ('code' in vals and journal.code != vals['code']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) @@ -536,6 +538,8 @@ class AccountJournal(models.Model): self.default_debit_account_id.currency_id = vals['currency_id'] if not 'default_credit_account_id' in vals and self.default_credit_account_id: self.default_credit_account_id.currency_id = vals['currency_id'] + if self.bank_account_id: + self.bank_account_id.currency_id = vals['currency_id'] if 'bank_account_id' in vals and not vals.get('bank_account_id'): raise UserError(_('You cannot remove the bank account from the journal once set.')) result = super(AccountJournal, self).write(vals) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e1e3dfa0499..47d7bcaf446 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -229,12 +229,6 @@ class AccountInvoice(models.Model): payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id')) self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)) - @api.depends('date_invoice', 'is_recurrency_enabled', 'recurrency_interval', 'recurrency_type') - def _compute_next_recurrency_date(self): - #initially set the recurrency_date - for invoice in self.filtered(lambda inv: inv.is_recurrency_enabled and inv.recurrency_interval and inv.recurrency_type and inv.date_invoice and not inv.next_recurrency_date): - invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.date_invoice) - name = fields.Char(string='Reference/Description', index=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False, help='The name that will be used on account move lines') @@ -373,18 +367,6 @@ class AccountInvoice(models.Model): #fields use to set the sequence, on the first invoice of the journal sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_number_next", inverse="_set_sequence_next") sequence_number_next_prefix = fields.Char(string='Next Number Prefix', compute="_get_sequence_prefix") - #recurrent vendor bills fields - is_recurrency_enabled = fields.Boolean(string="Generate Recurring Document(s)", copy=False, help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated.") - recurrency_interval = fields.Integer(string='Every', default=1, copy=False, help="Define the recurrency of the vendor bill.") - recurrency_type = fields.Selection([ - ('days', 'Day(s)'), - ('weeks', 'Week(s)'), - ('months', 'Month(s)'), - ('years', 'Year(s)'), - ], string="Recurrency Type", default='months', copy=False) - # Technical field depicting whether the document is generated by cron or by user - is_recurring_document = fields.Boolean(string="Recurring Document", copy=False) - next_recurrency_date = fields.Date(compute=_compute_next_recurrency_date, _string='Next Recurrency Date', copy=False, store=True) _sql_constraints = [ ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'), @@ -468,52 +450,6 @@ class AccountInvoice(models.Model): self.type == 'in_refund' and self.state == 'draft' and _('Vendor Credit Note') or \ self.type == 'in_refund' and _('Vendor Credit Note - %s') % (self.number) - def _get_next_recurrency_date(self, current_date): - """ - This method is used to return next date based on recurrency configuration of the record. - - :param current_date: Origin date from which next date is to be generated. - :returns: future date on which a duplicated document will be generated by crons. - """ - assert self.recurrency_type, "Invalid `Recurrency` interval! Please set it to either of these: 'Day(s)', 'Week(s)', 'Month(s)' or 'Year(s)'." - self.ensure_one() - recurring_next_date = fields.Date.from_string(current_date) + relativedelta(**{self.recurrency_type: self.recurrency_interval}) - # In case of monthly recurrent invoice, future invoices should always be created on same day as of it's main invoice but - # if the reference invoice was made on last day of the month, then the recurring invoice should be created on last day of next month. - # example, main invoice is set to 31st Jan, the next invoices should be created on 28/29th Feb, 31st Mar, 30 Apr and so on - if self.recurrency_type == 'months': - reference_invoice_day = fields.Date.from_string(self.date_invoice).day - recurring_next_date += relativedelta(day=reference_invoice_day) - if reference_invoice_day == fields.Date.from_string(self.date_invoice) + relativedelta(day=31): - recurring_next_date += relativedelta(day=31) - return recurring_next_date - - @api.model - def _cron_generate_recurring_vendor_bill(self): - today = fields.Date.today() - domain = [ - ('is_recurrency_enabled', '=', True), - ('state', 'not in', ['draft', 'cancel']), - ('next_recurrency_date', '<=', today) - ] - for invoice in self.search(domain): - while invoice.next_recurrency_date <= today: - invoice._create_recurrent_invoice() - invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.next_recurrency_date) - - def _create_recurrent_invoice(self): - self.ensure_one() - invoice = self.copy({ - 'origin': '', - 'date_invoice': self.next_recurrency_date, - 'is_recurring_document': True, - }) - # set the due date - #invoice._onchange_payment_term_date_invoice() - link = '%s' % (self.id, self.number) - invoice.message_post(body=_('This bill has been generated by %s') % link) - return invoice - @api.model def create(self, vals): onchanges = { @@ -1862,6 +1798,7 @@ class AccountPaymentTermLine(models.Model): if self.value == 'percent' and (self.value_amount < 0.0 or self.value_amount > 100.0): raise ValidationError(_('Percentages for Payment Terms Line must be between 0 and 100.')) + @api.one @api.constrains('days') def _check_days(self): if self.option in ('day_following_month', 'day_current_month') and self.days <= 0: diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index e66a6ab068e..a769a3f8d17 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -341,12 +341,13 @@ class account_journal(models.Model): ctx.update({ 'journal_type': self.type, 'default_journal_id': self.id, - 'search_default_journal_id': self.id, 'default_type': invoice_type, 'type': invoice_type }) [action] = self.env.ref('account.%s' % action_name).read() + if not self.env.context.get('use_domain'): + ctx['search_default_journal_id'] = self.id action['context'] = ctx action['domain'] = self._context.get('use_domain', []) account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 78e9cc0e3e9..f5196eb02d6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -106,7 +106,7 @@ class AccountMove(models.Model): default=lambda self: self.env.user.company_id) matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") # Dummy Account field to search on account.move by account_id - dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) + dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True) tax_cash_basis_rec_id = fields.Many2one( 'account.partial.reconcile', string='Tax Cash Basis Entry of', @@ -318,7 +318,7 @@ class AccountMoveLine(models.Model): if not cr.fetchone(): cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)') - @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state') + @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state') def _amount_residual(self): """ Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency. This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount @@ -983,7 +983,7 @@ class AccountMoveLine(models.Model): record.payment_id.state = 'reconciled' result = super(AccountMoveLine, self).write(vals) - if self._context.get('check_move_validity', True): + if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): move_ids = set() for line in self: if line.move_id.id not in move_ids: diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 2090d03bc30..ca1e13511d4 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -588,9 +588,9 @@ class account_payment(models.Model): writeoff_line['amount_currency'] = amount_currency_wo writeoff_line['currency_id'] = currency_id writeoff_line = aml_obj.create(writeoff_line) - if counterpart_aml['debit'] or writeoff_line['credit']: + if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']): counterpart_aml['debit'] += credit_wo - debit_wo - if counterpart_aml['credit'] or writeoff_line['debit']: + if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']): counterpart_aml['credit'] += debit_wo - credit_wo counterpart_aml['amount_currency'] -= amount_currency_wo diff --git a/addons/account/models/company.py b/addons/account/models/company.py index d73dbf82c64..8ae4230a7b2 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -4,7 +4,7 @@ from datetime import timedelta, datetime import calendar from odoo import fields, models, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from odoo.exceptions import UserError from odoo.tools.float_utils import float_round, float_is_zero @@ -130,6 +130,12 @@ Best Regards,''')) if values.get('cash_account_code_prefix'): new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code) + + #forbid the change of currency_id if there are already some accounting entries existing + if 'currency_id' in values and values['currency_id'] != company.currency_id.id: + if self.env['account.move.line'].search([('company_id', '=', company.id)]): + raise UserError(_('You cannot change the currency of the company since some journal items already exist')) + return super(ResCompany, self).write(values) @api.model diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 78a743ce9d7..68d8ef3e83b 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -57,12 +57,10 @@ class ResConfigSettings(models.TransientModel): tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility') tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal") account_hide_setup_bar = fields.Boolean(string='Hide Setup Bar', related='company_id.account_setup_bar_closed',help="Tick if you wish to hide the setup bar on the dashboard") - group_allow_recurring = fields.Boolean(string="Recurring Vendor Bills", implied_group="account.group_allow_recurring") @api.multi def set_values(self): super(ResConfigSettings, self).set_values() - self.env.ref('account.recurrent_vendor_bills_cron').write({'active': self.group_allow_recurring}) if self.group_multi_currency: self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]}) """ install a chart of accounts for the given company (if required) """ diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 1bcf45e5121..c527729765c 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -32,11 +32,6 @@ - - Allow to create recurring vendor bills - - - Allow the cash rounding management diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 8108b0efa09..9003e9e7f4d 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -426,20 +426,24 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { this._renderCreate(state); } var data = this.model.get(this.handleCreateRecord).data; - - this.model.notifyChanges(this.handleCreateRecord, state.createForm); - this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}); - _.each(state.createForm.analytic_tag_ids, function (tag) { - self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}); - }); - - var record = this.model.get(this.handleCreateRecord); - _.each(this.fields, function (field, fieldName) { - if (self._avoidFieldUpdate[fieldName]) return; - if (fieldName === "partner_id") return; - if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { - field.reset(record); - } + this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () { + // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids + self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){ + var defs = []; + _.each(state.createForm.analytic_tag_ids, function (tag) { + defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}})); + }); + $.when.apply($, defs).then(function () { + var record = self.model.get(self.handleCreateRecord); + _.each(self.fields, function (field, fieldName) { + if (self._avoidFieldUpdate[fieldName]) return; + if (fieldName === "partner_id") return; + if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { + field.reset(record); + } + }); + }); + }); }); } this.$('.create .add_line').toggle(!!state.balance.amount_currency); @@ -655,7 +659,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { return; } if(event.keyCode === 13) { - if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) { + var created_lines = _.findWhere(this.model.lines, {mode: 'create'}); + if (created_lines && created_lines.balance.amount) { this._onCreateProposition(); } return; diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 6a26ad45f2a..13427bb6870 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -43,18 +43,23 @@ var db = { fields: { id: {string: "ID", type: 'integer'}, code: {string: "code", type: 'integer'}, - display_name: {string: "Displayed name", type: 'char'}, + name: {string: "Displayed name", type: 'char'}, company_id: {string: "Company", type: 'many2one', relation: 'res.company'}, }, records: [ - {id: 282, code: 100000, display_name: "100000 Fixed Asset Account", company_id: 1}, - {id: 283, code: 101000, display_name: "101000 Current Assets", company_id: 1}, - {id: 284, code: 101110, display_name: "101110 Stock Valuation Account", company_id: 1}, - {id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)", company_id: 1}, - {id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)", company_id: 1}, - {id: 287, code: 101200, display_name: "101200 Account Receivable", company_id: 1}, - {id: 288, code: 101300, display_name: "101300 Tax Paid", company_id: 1}, - {id: 308, code: 101401, display_name: "101401 Bank", company_id: 1}, + {id: 282, code: 100000, name: "100000 Fixed Asset Account", company_id: 1}, + {id: 283, code: 101000, name: "101000 Current Assets", company_id: 1}, + {id: 284, code: 101110, name: "101110 Stock Valuation Account", company_id: 1}, + {id: 285, code: 101120, name: "101120 Stock Interim Account (Received)", company_id: 1}, + {id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)", company_id: 1}, + {id: 287, code: 101200, name: "101200 Account Receivable", company_id: 1}, + {id: 288, code: 101300, name: "101300 Tax Paid", company_id: 1}, + {id: 308, code: 101401, name: "101401 Bank", company_id: 1}, + {id: 500, code: 500, name: "500 Account", company_id: 1}, + {id: 501, code: 501, name: "501 Account", company_id: 1}, + {id: 502, code: 502, name: "502 Account", company_id: 1}, + {id: 503, code: 503, name: "503 Account", company_id: 1}, + {id: 504, code: 504, name: "504 Account", company_id: 1}, ], mark_as_reconciled: function () { return $.when(); @@ -1050,13 +1055,77 @@ QUnit.module('account', { clientAction.destroy(); }); + QUnit.test('Reconciliation create line (many2one test)', function (assert) { + assert.expect(5); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + var def = $.Deferred(); + + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + archs: { + "account.account,false,list": '', + "account.account,false,search": '', + }, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + return def.then(this._super.bind(this, route, args)); + } + return this._super(route, args); + }, + }); + + clientAction.prependTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + // open the first line in write-off mode + widget.$('.accounting_view tfoot td:first').trigger('click'); + + // select an account with the many2one (drop down) + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click'); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account"); + + // use the many2one select dialog to change the account + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click'); + // select the account who does not appear in the drop drown + $('.modal tr.o_data_row:contains(502)').click(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change"); + // wait the name_get to render the changes + def.resolve(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account"); + clientAction.destroy(); + }); + QUnit.test('Reconciliation create line with taxes', function (assert) { assert.expect(13); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, }); clientAction.appendTo($('#qunit-fixture')); @@ -1068,26 +1137,26 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_amount input').val('1100').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); clientAction.destroy(); @@ -1099,7 +1168,7 @@ QUnit.module('account', { var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + data: this.params.data, }); clientAction.appendTo($('#qunit-fixture')); diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 813896bdec0..d50e277028c 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -6,7 +6,6 @@ from . import test_account_move_closed_period from . import test_account_supplier_invoice from . import test_account_validate_account_move from . import test_account_invoice_rounding -from . import test_account_supplier_invoice_recurrent from . import test_bank_statement_reconciliation from . import test_fiscal_position from . import test_reconciliation_widget diff --git a/addons/account/tests/test_account_supplier_invoice_recurrent.py b/addons/account/tests/test_account_supplier_invoice_recurrent.py deleted file mode 100644 index 47f681dd3cb..00000000000 --- a/addons/account/tests/test_account_supplier_invoice_recurrent.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime -from dateutil.relativedelta import relativedelta - -from odoo.addons.account.tests.account_test_classes import AccountingTestCase -from odoo.tests import tagged - - -@tagged('post_install', '-at_install') -class TestAccountRecurrent(AccountingTestCase): - - def apply_vendor_cron(self): - self.env.ref('account.recurrent_vendor_bills_cron').method_direct_trigger() - - def test_create_recurring_vendor_bills(self): - AccountInvoice = self.env['account.invoice'] - invoice_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_receivable'))], limit=1).id - invoice_line_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_expenses'))], limit=1).id - purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1) - - invoice_line_data = [ - (0, 0, - { - 'product_id': self.ref('product.product_product_4'), - 'name': 'product 4 that cost 100', - 'quantity': 1.0, - 'price_unit': 100.0, - 'account_id': invoice_line_account_id, - } - ), - (0, 0, - { - 'product_id': self.ref('product.product_product_5'), - 'name': 'product 5 that cost 200', - 'quantity': 2.0, - 'price_unit': 200.0, - 'account_id': invoice_line_account_id, - } - ) - ] - - # Create an invoice dated 2 months before today - invoice = AccountInvoice.create({ - 'partner_id': self.ref('base.res_partner_2'), - 'account_id': invoice_account_id, - 'name': 'invoice test recurrent', - 'type': 'in_invoice', - 'reference_type': 'none', - 'date_invoice': datetime.today() + relativedelta(months=-2), - 'journal_id': purchase_journal.id, - 'is_recurrency_enabled': True, - 'recurrency_interval': 1, - 'recurrency_type': 'months', - 'invoice_line_ids': invoice_line_data - }) - recurring_domain = [('type', '=', 'in_invoice'), ('state', '=', 'draft'), ('is_recurring_document', '=', True)] - - # After executing cron, verify that no bill is auto generated if the recurrent bill is in `draft` state - self.apply_vendor_cron() - recurring_invoice_count = AccountInvoice.search_count(recurring_domain) - self.assertEquals(recurring_invoice_count, 0, 'Recurring bills should not be generated when reference bill is still `draft`.') - - # After validating bill, run cron and check that 2 recurring bill have been generated - invoice.action_invoice_open() # Validate invoice - self.apply_vendor_cron() - recurring_invoice_count = previous_recurring_invoice_count = AccountInvoice.search_count(recurring_domain) - self.assertEquals(recurring_invoice_count, 2, '2 recurring bills should be generated.') - - # verify that bills aren't generated if reference bill is in `cancel` state - purchase_journal.write({'update_posted': True}) # Allow to cancel the invoice - inv2_data = { - 'name': 'invoice test recurrent 2', - 'date_invoice': datetime.today() + relativedelta(months=-2), - 'is_recurrency_enabled': True, - 'recurrency_interval': 1, - 'recurrency_type': 'months', - } - invoice2 = invoice.copy(default=inv2_data) - invoice2.action_invoice_cancel() # Cancel the invoice - self.apply_vendor_cron() - recurring_invoice_count = AccountInvoice.search_count(recurring_domain) - self.assertEquals(previous_recurring_invoice_count, recurring_invoice_count, 'Recurring bills should not be generated when reference bill is in `cancel` state.') - - # verify that vendor bills are generated even when invoice is in `paid` state - inv3_data = { - 'name': 'invoice test recurrent 3', - 'date_invoice': datetime.today() + relativedelta(months=-2), - 'is_recurrency_enabled': True, - 'recurrency_interval': 1, - 'recurrency_type': 'months', - } - - invoice3 = invoice.copy(default=inv3_data) - invoice3.action_invoice_open() # Validate invoice - Payment = self.env['account.payment'] - payment_vals = Payment.with_context(default_invoice_ids=[(4, invoice3.id, None)]).default_get(Payment._fields.keys()) - payment_vals.update({ - 'payment_method_id': self.ref("account.account_payment_method_manual_out"), - 'journal_id': self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id - }) - Payment.create(payment_vals).action_validate_invoice_payment() # Register the payment - self.apply_vendor_cron() - recurring_invoice_count = AccountInvoice.search_count(recurring_domain) - self.assertEquals(recurring_invoice_count, 4, '4 recurring bills should be generated.') diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 3c22a127180..3d490eedc49 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -20,7 +20,9 @@ class TestPayment(AccountingTestCase): self.currency_chf_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_eur_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id}) + + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id]) self.product = self.env.ref("product.product_product_4") self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in") self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out") @@ -361,3 +363,53 @@ class TestPayment(AccountingTestCase): {'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, {'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False}, ]) + + def test_payment_and_writeoff_in_other_currency(self): + # Use case: + # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id}, + ]) + # Use case: + # Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'supplier', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id}, + ]) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index aa4fc950279..1180df172c9 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -27,7 +27,8 @@ class TestReconciliation(AccountingTestCase): self.currency_swiss_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_euro_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id}) + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id]) self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1) self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1) self.product = self.env.ref("product.product_product_4") diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index b75bd604ee2..30b9382e607 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -339,12 +339,6 @@ - -

- - -
@@ -541,10 +535,8 @@ - - - - + + @@ -552,8 +544,6 @@ - - @@ -578,36 +568,6 @@ - - account.invoice.select.invoices - account.invoice - primary - - - - - Not Draft - - - - - - - - account.invoice.select.credit.notes - account.invoice - primary - - - - - Not Draft - - - - - - Invoices account.invoice @@ -675,7 +635,7 @@ [('type','=','out_invoice')] {'type':'out_invoice', 'journal_type': 'sale'} - +

Create a customer invoice @@ -710,7 +670,7 @@ [('type','=','out_refund')] {'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'} - +

Create a credit note @@ -748,7 +708,7 @@ [('type','=','in_invoice')] {'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'} - +

Record a new vendor bill @@ -784,7 +744,7 @@ [('type','=','in_refund')] {'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'} - +

Create a vendor credit note diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index dfa08fbadce..9c7f1aca551 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -117,16 +117,12 @@ View

Payments Matching diff --git a/addons/account/views/account_report.xml b/addons/account/views/account_report.xml index 06e077e7d8c..2ba116e884a 100644 --- a/addons/account/views/account_report.xml +++ b/addons/account/views/account_report.xml @@ -56,6 +56,7 @@ id="action_report_financial" model="account.financial.report" string="Financial report" + menu="False" report_type="qweb-pdf" name="account.report_financial" file="account.report_financial" diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 7b57066bce8..6d8ffdd1ac0 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -16,7 +16,7 @@ - + diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 5ba661a110f..179699fa726 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -446,20 +446,9 @@
-

Automated Entries

-