diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-21 15:37:19 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-21 15:37:19 +0100 |
| commit | 1d2b6f1bef6af13245d29d56cbb5f9d61f9ca038 (patch) | |
| tree | 8ea1d203aef83d287de9ec578f81bd64eedb7359 | |
| parent | f945b1bcc21621cfc056e40f4d66b7181d19239e (diff) | |
| parent | 84143a34b313c01b0b37b7d7246ce3d5c9476002 (diff) | |
[MERGE] forward port branch 12.0 up to 84143a34b31
151 files changed, 4357 insertions, 1730 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index ec97bafd1c6..f42268edd70 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -481,7 +481,7 @@ class AccountJournal(models.Model): post_at_bank_rec = fields.Boolean(string="Post At Bank Reconciliation", help="Whether or not the payments made in this journal should be generated in draft state, so that the related journal entries are only posted when performing bank reconciliation.") # alias configuration for 'purchase' type journals - alias_id = fields.Many2one('mail.alias', string='Alias') + alias_id = fields.Many2one('mail.alias', string='Alias', copy=False) alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")) alias_name = fields.Char('Alias Name for Vendor Bills', related='alias_id.alias_name', help="It creates draft vendor bill by sending an email.", readonly=False) diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index d0632970bed..d22962ebbd0 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -587,8 +587,11 @@ class AccountBankStatementLine(models.Model): # Fully reconciled moves are just linked to the bank statement total = self.amount + currency = self.currency_id or statement_currency for aml_rec in payment_aml_rec: - total -= aml_rec.debit - aml_rec.credit + balance = aml_rec.amount_currency if aml_rec.currency_id else aml_rec.balance + aml_currency = aml_rec.currency_id or aml_rec.company_currency_id + total -= aml_currency._convert(balance, currency, aml_rec.company_id, aml_rec.date) aml_rec.with_context(check_move_validity=False).write({'statement_line_id': self.id}) counterpart_moves = (counterpart_moves | aml_rec.move_id) if aml_rec.journal_id.post_at_bank_rec and aml_rec.payment_id and aml_rec.move_id.state == 'draft': diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 6d4ef94ddd9..1d1483b2335 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -384,18 +384,20 @@ class AccountMove(models.Model): @api.multi def _reverse_move(self, date=None, journal_id=None, auto=False): self.ensure_one() - reversed_move = self.copy(default={ - 'date': date, - 'journal_id': journal_id.id if journal_id else self.journal_id.id, - 'ref': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (self.name), - 'auto_reverse': False}) - for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): - acm_line.write({ - 'debit': acm_line.credit, - 'credit': acm_line.debit, - 'amount_currency': -acm_line.amount_currency - }) - self.reverse_entry_id = reversed_move + with self.env.norecompute(): + reversed_move = self.copy(default={ + 'date': date, + 'journal_id': journal_id.id if journal_id else self.journal_id.id, + 'ref': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (self.name), + 'auto_reverse': False}) + for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): + acm_line.write({ + 'debit': acm_line.credit, + 'credit': acm_line.debit, + 'amount_currency': -acm_line.amount_currency + }) + self.reverse_entry_id = reversed_move + self.recompute() return reversed_move @api.multi @@ -524,7 +526,7 @@ class AccountMoveLine(models.Model): #computing the `reconciled` field. reconciled = False digits_rounding_precision = line.company_id.currency_id.rounding - if float_is_zero(amount, precision_rounding=digits_rounding_precision): + if (line.matched_debit_ids or line.matched_credit_ids) and float_is_zero(amount, precision_rounding=digits_rounding_precision): if line.currency_id and line.amount_currency: if float_is_zero(amount_residual_currency, precision_rounding=line.currency_id.rounding): reconciled = True @@ -905,7 +907,7 @@ class AccountMoveLine(models.Model): for line in self: company_ids.add(line.company_id.id) all_accounts.append(line.account_id) - if line.reconciled: + if (line.matched_debit_ids or line.matched_credit_ids) and line.reconciled: raise UserError(_('You are trying to reconcile some entries that are already reconciled.')) if len(company_ids) > 1: raise UserError(_('To reconcile the entries company should be the same for all entries.')) @@ -1053,6 +1055,56 @@ class AccountMoveLine(models.Model): ) return rec_move_ids.unlink() + def _apply_taxes(self, vals, amount): + tax_lines_vals = [] + # Get ids from triplets : https://www.odoo.com/documentation/10.0/reference/orm.html#odoo.models.Model.write + tax_ids = [tax['id'] for tax in self.resolve_2many_commands('tax_ids', vals['tax_ids']) if tax.get('id')] + # Since create() receives ids instead of recordset, let's just use the old-api bridge + taxes = self.env['account.tax'].browse(tax_ids) + currency = self.env['res.currency'].browse(vals.get('currency_id')) + partner = self.env['res.partner'].browse(vals.get('partner_id')) + ctx = dict(self._context) + ctx['round'] = ctx.get('round', True) + res = taxes.with_context(ctx).compute_all(amount, + currency, 1, vals.get('product_id'), partner) + # Adjust line amount if any tax is price_include + if abs(res['total_excluded']) < abs(amount): + if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] + if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] + if vals.get('amount_currency'): + vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (res['total_excluded']/amount)) + # Create tax lines + for tax_vals in res['taxes']: + if tax_vals['amount']: + tax = self.env['account.tax'].browse([tax_vals['id']]) + account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) + if not account_id: account_id = vals['account_id'] + temp = { + 'account_id': account_id, + 'name': vals['name'] + ' ' + tax_vals['name'], + 'tax_line_id': tax_vals['id'], + 'move_id': vals['move_id'], + 'partner_id': vals.get('partner_id'), + 'statement_id': vals.get('statement_id'), + 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, + 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, + 'analytic_account_id': vals.get('analytic_account_id') if tax.analytic else False, + } + bank = self.env["account.bank.statement.line"].browse(vals.get('statement_line_id')).statement_id + if bank.currency_id != bank.company_id.currency_id: + ctx = {} + if 'date' in vals: + ctx['date'] = vals['date'] + elif 'date_maturity' in vals: + ctx['date'] = vals['date_maturity'] + temp['currency_id'] = bank.currency_id.id + temp['amount_currency'] = bank.company_id.currency_id.with_context(ctx).compute(tax_vals['amount'], bank.currency_id, round=True) + if vals.get('tax_exigible'): + temp['tax_exigible'] = True + temp['account_id'] = tax.cash_basis_account.id or account_id + tax_lines_vals.append(temp) + return tax_lines_vals + #################################################### # CRUD methods #################################################### @@ -1486,90 +1538,91 @@ class AccountPartialReconcile(models.Model): self.ensure_one() move_date = self.debit_move_id.date newly_created_move = self.env['account.move'] - for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): - #move_date is the max of the 2 reconciled items - if move_date < move.date: - move_date = move.date - percentage_before = percentage_before_rec[move.id] - percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] - # update the percentage before as the move can be part of - # multiple partial reconciliations - percentage_before_rec[move.id] = percentage_after - - for line in move.line_ids: - if not line.tax_exigible: - #amount is the current cash_basis amount minus the one before the reconciliation - amount = line.balance * percentage_after - line.balance * percentage_before - rounded_amt = self._get_amount_tax_cash_basis(amount, line) - if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): - continue - if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the tax line - to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.move_id.name, - 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'credit': rounded_amt if rounded_amt > 0 else 0.0, - 'account_id': line.account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - # Group by cash basis account and tax - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt if rounded_amt > 0 else 0.0, - 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'account_id': line.tax_line_id.cash_basis_account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_line_id': line.tax_line_id.id, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - if line.account_id.reconcile: - #setting the account to allow reconciliation will help to fix rounding errors - to_clear_aml |= line - to_clear_aml.reconcile() - - if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the base - for tax in line.tax_ids.filtered(lambda t: t.tax_exigibility == 'on_payment'): - account_id = self._get_tax_cash_basis_base_account(line, tax) - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt > 0 and rounded_amt or 0.0, - 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + with self.env.norecompute(): + for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): + #move_date is the max of the 2 reconciled items + if move_date < move.date: + move_date = move.date + percentage_before = percentage_before_rec[move.id] + percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] + # update the percentage before as the move can be part of + # multiple partial reconciliations + percentage_before_rec[move.id] = percentage_after + + for line in move.line_ids: + if not line.tax_exigible: + #amount is the current cash_basis amount minus the one before the reconciliation + amount = line.balance * percentage_after - line.balance * percentage_before + rounded_amt = self._get_amount_tax_cash_basis(amount, line) + if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): + continue + if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the tax line + to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.move_id.name, + 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'credit': rounded_amt if rounded_amt > 0 else 0.0, + 'account_id': line.account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, 'tax_exigible': True, - 'tax_ids': [(6, 0, [tax.id])], - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) + # Group by cash basis account and tax self.env['account.move.line'].with_context(check_move_validity=False).create({ 'name': line.name, - 'credit': rounded_amt > 0 and rounded_amt or 0.0, - 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + 'debit': rounded_amt if rounded_amt > 0 else 0.0, + 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'account_id': line.tax_line_id.cash_basis_account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, + 'tax_line_id': line.tax_line_id.id, 'tax_exigible': True, - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) - + if line.account_id.reconcile: + #setting the account to allow reconciliation will help to fix rounding errors + to_clear_aml |= line + to_clear_aml.reconcile() + + if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the base + for tax in line.tax_ids.filtered(lambda t: t.tax_exigibility == 'on_payment'): + account_id = self._get_tax_cash_basis_base_account(line, tax) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'debit': rounded_amt > 0 and rounded_amt or 0.0, + 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'tax_ids': [(6, 0, [tax.id])], + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + '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, + }) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'credit': rounded_amt > 0 and rounded_amt or 0.0, + 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + '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, + }) + self.recompute() if newly_created_move: if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date: # The move date should be the maximum date between payment and invoice (in case diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index bb2e7ff4f03..7dffbaa2233 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -211,6 +211,8 @@ class account_abstract_payment(models.AbstractModel): self.amount = abs(self._compute_payment_amount()) # Set by default the first liquidity journal having this currency if exists. + if self.journal_id: + return journal = self.env['account.journal'].search( [('type', 'in', ('bank', 'cash')), ('currency_id', '=', self.currency_id.id)], limit=1) if journal: diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 717b99ad26d..bfbab8809ca 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -173,7 +173,7 @@ class AccountChartTemplate(models.Model): # (won't work well for multi-company) company = self.env.user.company_id # If we don't have any chart of account on this company, install this chart of account - if not self.existing_accounting(company): + if not company.chart_template_id and not self.existing_accounting(company): self.load_for_current_company(15.0, 15.0) def load_for_current_company(self, sale_tax_rate, purchase_tax_rate): diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index ef09176438b..a091adbd8b8 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -68,7 +68,7 @@ access_account_account_type_invoice,account.account.type invoice,model_account_a access_account_account_tax,account.account.tag,model_account_account_tag,account.group_account_user,1,1,1,1 access_account_account_tax_user,account.account.tag,model_account_account_tag,account.group_account_invoice,1,0,0,0 access_account_reconcile_model,account.reconcile.model,model_account_reconcile_model,account.group_account_user,1,1,1,1 -access_account_reconcile_model_template,account.reconcile.model.template,model_account_reconcile_model_template,account.group_account_user,1,1,1,1 +access_account_reconcile_model_template,account.reconcile.model.template,model_account_reconcile_model_template,account.group_account_invoice,1,1,1,1 access_account_partial_reconcile_group_invoice,account.partial.reconcile.group.invoice,model_account_partial_reconcile,account.group_account_invoice,1,1,1,0 access_account_partial_reconcile,account.partial.reconcile,model_account_partial_reconcile,account.group_account_user,1,1,1,1 access_account_full_reconcile_group_invoice,account.full.reconcile.group.invoice,model_account_full_reconcile,account.group_account_invoice,1,1,1,0 diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 06387df0579..d948895bda4 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -904,11 +904,11 @@ var StatementModel = BasicModel.extend({ }); var company_currency = session.get_currency(line.st_line.currency_id); var company_precision = company_currency && company_currency.digits[1] || 2; - total = utils.round_decimals(total*1000, company_precision)/1000 || 0; + total = utils.round_decimals(total, company_precision) || 0; if(isOtherCurrencyId){ var other_currency = session.get_currency(isOtherCurrencyId); var other_precision = other_currency && other_currency.digits[1] || 2; - amount_currency = utils.round_decimals(amount_currency, other_precision) + amount_currency = utils.round_decimals(amount_currency, other_precision); } line.balance = { amount: total, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index b5e97566936..cbc5b8bdcd6 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -260,7 +260,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'click .accounting_view thead td': '_onTogglePanel', 'click .accounting_view tfoot td:not(.cell_left,.cell_right)': '_onShowPanel', 'click .cell': '_onEditAmount', - 'input input.filter': '_onFilterChange', + 'change input.filter': '_onFilterChange', 'click .match .load-more a': '_onLoadMore', 'click .match .mv_line td': '_onSelectMoveLine', 'click .accounting_view tbody .mv_line td': '_onSelectProposition', diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index d07ea5a6e60..c8cd06d258e 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -144,7 +144,8 @@ <div t-name="reconciliation.line.match"> <div class="match_controls"> - <input class="filter" placeholder="Filter..." type="text" value=""/> + <input class="filter o_input" placeholder="Filter..." type="text" value=""/> + <button class="btn btn-secondary btn-sm fa fa-search" type="button"></button> </div> <table> <tbody> diff --git a/addons/account/tests/account_test_no_chart.py b/addons/account/tests/account_test_no_chart.py index 05aa54df4c6..ca1c971d0a6 100644 --- a/addons/account/tests/account_test_no_chart.py +++ b/addons/account/tests/account_test_no_chart.py @@ -46,6 +46,12 @@ class TestAccountNoChartCommon(SavepointCase): 'property_account_receivable_id': cls.account_receivable.id, }) + cls.sale_journal0 = cls.env['account.journal'].create({ + 'name': 'Sale Journal', + 'type': 'sale', + 'code': 'SJT0', + }) + @classmethod def setUpAdditionalAccounts(cls): """ Set up some addionnal accounts: expenses, revenue, ... """ diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 6963017a41a..eab1c9f42fb 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -529,6 +529,14 @@ class TestReconciliation(AccountingTestCase): self.diff_income_account.id: {'debit': 0.0, 'credit': 3.27, 'amount_currency': -5, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 52.33, 'amount_currency': -80, 'currency_id': self.currency_usd_id}, } + + payments = bank_stmt_aml.mapped('payment_id') + # creation and reconciliation of the over-amount statement + # has created an another payment + self.assertEqual(len(payments), 2) + # Check amount of second, automatically created payment + self.assertEqual((payments - payment).amount, 5) + for aml in bank_stmt_aml: line = lines[aml.account_id.id] if type(line) == list: diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index e91e0ef2a3b..a468a45fee2 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -96,7 +96,6 @@ </div> <div class="o_setting_right_pane" name="account_taxcloud_right_pane"> <label for="module_account_taxcloud" string="TaxCloud"/> - <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/> <div class="text-muted"> Compute tax rates based on U.S. ZIP codes </div> diff --git a/addons/account_facturx/models/account_invoice.py b/addons/account_facturx/models/account_invoice.py index 619bd64d354..3b5e080162e 100644 --- a/addons/account_facturx/models/account_invoice.py +++ b/addons/account_facturx/models/account_invoice.py @@ -64,12 +64,11 @@ class AccountInvoice(models.Model): with Form(self_ctx, view='account.invoice_supplier_form') as invoice_form: # Partner (first step to avoid warning 'Warning! You must first select a partner.'). - elements = tree.xpath('//ram:SellerTradeParty/ram:Name', namespaces=tree.nsmap) - partner_name = elements and elements[0].text - elements = tree.xpath('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1) if not partner: + elements = tree.xpath('//ram:SellerTradeParty/ram:Name', namespaces=tree.nsmap) + partner_name = elements and elements[0].text partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1) if not partner: elements = tree.xpath('//ram:SellerTradeParty//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap) @@ -161,19 +160,19 @@ class AccountInvoice(models.Model): if product: invoice_line_form.product_id = product + # Quantity. + line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap) + if line_elements: + invoice_line_form.quantity = float(line_elements[0].text) * refund_sign + # Price Unit. line_elements = element.xpath('.//ram:GrossPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap) if line_elements: - invoice_line_form.price_unit = float(line_elements[0].text) + invoice_line_form.price_unit = float(line_elements[0].text) / invoice_line_form.quantity else: line_elements = element.xpath('.//ram:NetPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap) if line_elements: - invoice_line_form.price_unit = float(line_elements[0].text) - - # Quantity. - line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap) - if line_elements: - invoice_line_form.quantity = float(line_elements[0].text) * refund_sign + invoice_line_form.price_unit = float(line_elements[0].text) / invoice_line_form.quantity # Discount. line_elements = element.xpath('.//ram:AppliedTradeAllowanceCharge/ram:CalculationPercent', namespaces=tree.nsmap) diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 2c5d9d9f30c..059e59bd35f 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -274,6 +274,8 @@ class AccountVoucher(models.Model): :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method) :rtype: tuple(float, list of int) ''' + tax_calculation_rounding_method = self.env.user.company_id.tax_calculation_rounding_method + tax_lines_vals = [] for line in self.line_ids: #create one move line per voucher line where amount is not 0.0 if not line.price_subtotal: @@ -331,6 +333,33 @@ class AccountVoucher(models.Model): self.env['account.move.line'].create(temp) self.env['account.move.line'].create(move_line) + # When global rounding is activated, we must wait until all tax lines are computed to + # merge them. + if tax_calculation_rounding_method == 'round_globally': + tax_lines_vals += self.env['account.move.line'].with_context(round=False)._apply_taxes( + move_line, + move_line.get('debit', 0.0) - move_line.get('credit', 0.0) + ) + + # When round globally is set, we merge the tax lines + if tax_calculation_rounding_method == 'round_globally': + tax_lines_vals_merged = {} + for tax_line_vals in tax_lines_vals: + key = ( + tax_line_vals['tax_line_id'], + tax_line_vals['account_id'], + tax_line_vals['analytic_account_id'], + ) + if key not in tax_lines_vals_merged: + tax_lines_vals_merged[key] = tax_line_vals + else: + tax_lines_vals_merged[key]['debit'] += tax_line_vals['debit'] + tax_lines_vals_merged[key]['credit'] += tax_line_vals['credit'] + currency = self.env['res.currency'].browse(company_currency) + for vals in tax_lines_vals_merged.values(): + vals['debit'] = currency.round(vals['debit']) + vals['credit'] = currency.round(vals['credit']) + self.env['account.move.line'].create(vals) return line_total @api.multi @@ -363,12 +392,12 @@ class AccountVoucher(models.Model): line_total = voucher.with_context(ctx).voucher_move_line_create(line_total, move.id, company_currency, current_currency) # Create a payment to allow the reconciliation when pay_now = 'pay_now'. - if self.pay_now == 'pay_now': - payment_id = self.env['account.payment'].create(self.voucher_pay_now_payment_create()) + if voucher.pay_now == 'pay_now': + payment_id = self.env['account.payment'].create(voucher.voucher_pay_now_payment_create()) payment_id.post() # Reconcile the receipt with the payment - lines_to_reconcile = (payment_id.move_line_ids + move.line_ids).filtered(lambda l: l.account_id == self.account_id) + lines_to_reconcile = (payment_id.move_line_ids + move.line_ids).filtered(lambda l: l.account_id == voucher.account_id) lines_to_reconcile.reconcile() # Add tax correction to move line if any tax correction specified diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 87a47ffa455..5703e018548 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -53,8 +53,8 @@ class ResPartner(models.Model): the url state components (menu_id, id, view_type) """ res = dict.fromkeys(self.ids, False) - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for partner in self: + base_url = partner.get_base_url() # when required, make sure the partner has a valid signup token if self.env.context.get('signup_valid') and not partner.user_ids: partner.sudo().signup_prepare() diff --git a/addons/base_automation/views/base_automation_view.xml b/addons/base_automation/views/base_automation_view.xml index 206f7897c75..35875ccc3ff 100644 --- a/addons/base_automation/views/base_automation_view.xml +++ b/addons/base_automation/views/base_automation_view.xml @@ -11,9 +11,6 @@ <xpath expr="//button[@name='create_action']" position="replace"> </xpath> <xpath expr="//button[@name='unlink_action']" position="replace"> - <button name="unlink_action_for_menu" string="Remove Action" type="object" - attrs="{'invisible':[('binding_model_id','=',False)]}" - help="Remove the contextual action related to this server action"/> </xpath> <div class="oe_title" position="before"> <div class="oe_button_box" name="button_box"> diff --git a/addons/base_import_module/views/base_import_module_view.xml b/addons/base_import_module/views/base_import_module_view.xml index fb18fdfbf49..13c666ad313 100644 --- a/addons/base_import_module/views/base_import_module_view.xml +++ b/addons/base_import_module/views/base_import_module_view.xml @@ -8,6 +8,7 @@ <form string="Import module"> <field name="state" invisible="1"/> <separator string="Import Module" colspan="4"/> + <p class="alert alert-warning" role="alert">Note: you can only import data modules (.xml files and static assets)</p> <group states="init" col="4"> <label for="module_file" string="Select module package to import (.zip file):" colspan="4"/> <field name="module_file" colspan="4"/> diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index 4764e2e52b0..a24dcb593fd 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -57,6 +57,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, { $(window).off("click." + this._id); $(window).off("keydown." + this._id); $(window).off("keyup." + this._id); + this._super(); }, //-------------------------------------------------------------------------- // Public diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index a7b873a2464..2858ac644cc 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -75,7 +75,7 @@ class Lead(models.Model): kanban_state = fields.Selection([('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')], string='Kanban State', compute='_compute_kanban_state') email_cc = fields.Text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") - description = fields.Text('Notes', track_visibility='onchange', track_sequence=60) + description = fields.Text('Notes') tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', string='Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service") contact_name = fields.Char('Contact Name', track_visibility='onchange', track_sequence=30) partner_name = fields.Char("Company Name", track_visibility='onchange', track_sequence=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity') diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 251a0e60503..e1d53844d79 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -969,7 +969,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid } </field> <field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/> @@ -1031,7 +1030,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid, 'search_default_assigned_to_me': 1 }</field> <field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/> diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml index f405892e692..e3053b661d1 100644 --- a/addons/digest/data/digest_template_data.xml +++ b/addons/digest/data/digest_template_data.xml @@ -10,7 +10,8 @@ <table style="width: 100%; border-spacing: 0; font-family: Helvetica,Arial,Verdana,sans-serif;"> <tr> <td align="center" valign="top" style="border-collapse: collapse; padding: 0"> - % set company, user = ctx['company'], ctx['user'] + % set user = ctx.get('user', user) + % set company = user.company_id % set data = object.compute_kpis(company, user) % set tips = object.compute_tips(company, user) % set kpi_actions = object.compute_kpis_actions(company, user) @@ -136,7 +137,7 @@ <table style="margin-top: 5px; border: 1px solid #e7e7e7; font-size: 15px; width: 100%; max-width: 600px;"> <tr> <td style="border-collapse: collapse; margin: 0; padding: 10px 20px;"> - % if ctx['user'].has_group('base.group_system'): + % if user.has_group('base.group_system'): <div style="margin-top: 20px;"> Want to customize the email? <a href="/web#view_type=form&model=digest.digest&id=${object.id}" target="_blank" style="color: #875A7B;">Choose the metrics you care about</a> diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 9446f5454a2..7809648e48c 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -101,7 +101,7 @@ class Digest(models.Model): for digest in self: for user in digest.user_ids: subject = '%s: %s' % (user.company_id.name, digest.name) - digest.template_id.with_context(user=user, company=user.company_id).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) + digest.template_id.with_context(user=user).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) digest.next_run_date = digest._get_next_run_date() def compute_kpis(self, company, user): diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index ac1793f428f..9155cf9a0fa 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -442,7 +442,9 @@ class GoogleCalendar(models.AbstractModel): if google_attendee.get('found'): continue - attendee = ResPartner.search([('email', '=', google_attendee['email'])], limit=1) + attendee = ResPartner.search([('email', '=ilike', google_attendee['email']), ('user_ids', '!=', False)], limit=1) + if not attendee: + attendee = ResPartner.search([('email', '=ilike', google_attendee['email'])], limit=1) if not attendee: data = { 'email': partner_email, diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index ca5eecab771..6ee85c74f18 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -640,6 +640,17 @@ </field> </record> + <record id="view_users_form_simple_modif_resource" model="ir.ui.view"> + <field name="name">res.users.preferences.form.resource</field> + <field name="model">res.users</field> + <field name="inherit_id" ref="base.view_users_form_simple_modif" /> + <field name="arch" type="xml"> + <field name="tz" position="attributes"> + <attribute name="required">1</attribute> + </field> + </field> + </record> + <menuitem action="open_module_tree_department" id="menu_hr_department_tree" diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js index 4db5eb90b0a..c7640527297 100644 --- a/addons/hr_attendance/static/src/js/kiosk_confirm.js +++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js @@ -9,9 +9,8 @@ var QWeb = core.qweb; var KioskConfirm = AbstractAction.extend({ events: { "click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, - "click .o_hr_attendance_sign_in_out_icon": function () { + "click .o_hr_attendance_sign_in_out_icon": _.debounce(function () { var self = this; - this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); this._rpc({ model: 'hr.employee', method: 'attendance_manual', @@ -22,10 +21,9 @@ var KioskConfirm = AbstractAction.extend({ self.do_action(result.action); } else if (result.warning) { self.do_warn(result.warning); - self.$('.o_hr_attendance_sign_in_out_icon').removeAttr("disabled"); } }); - }, + }, 200, true), 'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); }, 'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); }, 'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); }, @@ -37,7 +35,7 @@ var KioskConfirm = AbstractAction.extend({ 'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); }, 'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); }, 'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); }, - 'click .o_hr_attendance_pin_pad_button_ok': function() { + 'click .o_hr_attendance_pin_pad_button_ok': _.debounce(function() { var self = this; this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled"); this._rpc({ @@ -54,7 +52,7 @@ var KioskConfirm = AbstractAction.extend({ setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500); } }); - }, + }, 200, true), }, init: function (parent, action) { diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index 4ea44093949..b79469596af 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -8,10 +8,9 @@ var core = require('web.core'); var MyAttendances = AbstractAction.extend({ contentTemplate: 'HrAttendanceMyMainMenu', events: { - "click .o_hr_attendance_sign_in_out_icon": function() { - this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); + "click .o_hr_attendance_sign_in_out_icon": _.debounce(function() { this.update_attendance(); - }, + }, 200, true), }, willStart: function () { diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index a0e9ba65e8d..7679454a404 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -250,7 +250,10 @@ class HolidaysType(models.Model): if record.allocation_type != 'no': name = "%(name)s (%(count)s)" % { 'name': name, - 'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, float_round(record.max_leaves, precision_digits=2) or 0.0) + 'count': _('%g remaining out of %g') % ( + float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, + float_round(record.max_leaves, precision_digits=2) or 0.0, + ) } res.append((record.id, name)) return res diff --git a/addons/hr_payroll_account/models/hr_payroll_account.py b/addons/hr_payroll_account/models/hr_payroll_account.py index 1e21ae1a83a..76ffd66ad91 100644 --- a/addons/hr_payroll_account/models/hr_payroll_account.py +++ b/addons/hr_payroll_account/models/hr_payroll_account.py @@ -54,13 +54,13 @@ class HrPayslip(models.Model): @api.multi def action_payslip_done(self): res = super(HrPayslip, self).action_payslip_done() - precision = self.env['decimal.precision'].precision_get('Payroll') for slip in self: line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip.date or slip.date_to + currency = slip.company_id.currency_id name = _('Payslip of %s') % (slip.employee_id.name) move_dict = { @@ -70,8 +70,8 @@ class HrPayslip(models.Model): 'date': date, } for line in slip.line_ids.filtered(lambda line: line.category_id): - amount = slip.credit_note and -line.total or line.total - if float_is_zero(amount, precision_digits=precision): + amount = currency.round(slip.credit_note and -line.total or line.total) + if currency.is_zero(amount): continue debit_account_id = line.salary_rule_id.account_debit.id credit_account_id = line.salary_rule_id.account_credit.id @@ -106,7 +106,7 @@ class HrPayslip(models.Model): line_ids.append(credit_line) credit_sum += credit_line[2]['credit'] - credit_line[2]['debit'] - if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: + if currency.compare_amounts(credit_sum, debit_sum) == -1: acc_id = slip.journal_id.default_credit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name)) @@ -117,11 +117,11 @@ class HrPayslip(models.Model): 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, - 'credit': debit_sum - credit_sum, + 'credit': currency.round(debit_sum - credit_sum), }) line_ids.append(adjust_credit) - elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: + elif currency.compare_amounts(debit_sum, credit_sum) == -1: acc_id = slip.journal_id.default_debit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (slip.journal_id.name)) @@ -131,7 +131,7 @@ class HrPayslip(models.Model): 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, - 'debit': credit_sum - debit_sum, + 'debit': currency.round(credit_sum - debit_sum), 'credit': 0.0, }) line_ids.append(adjust_debit) diff --git a/addons/l10n_ch/i18n_extra/de.po b/addons/l10n_ch/i18n_extra/de.po index acfab8ce872..278f2bf8a0f 100644 --- a/addons/l10n_ch/i18n_extra/de.po +++ b/addons/l10n_ch/i18n_extra/de.po @@ -4,26 +4,25 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0c\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" -"PO-Revision-Date: 2016-12-15 14:35+0100\n" -"Last-Translator: <didier.donze@camptocamp.com>\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" +"PO-Revision-Date: 2019-02-12 12:06+0530\n" +"Last-Translator: didier.donze@camptocamp.com\n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" +"X-Generator: Poedit 2.2.1\n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "0% Exkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "0% Import." @@ -34,118 +33,105 @@ msgid "100% dédouanement TVA" msgstr "100% Verzollung MwSt." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "100% Imp." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "2.5%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "2.5% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "2.5% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "2.5% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "2.5% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "2.5% Invest. Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" -msgstr "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" +msgstr "3.7%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." -msgstr "3.8% Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." +msgstr "3.7% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" -msgstr "3.8% Einkauf" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" +msgstr "3.7% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." -msgstr "3.8% Einkauf Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." +msgstr "3.7% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" -msgstr "3.8% Invest." +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" +msgstr "3.7% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." -msgstr "3.8% Invest. Inkl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." +msgstr "3.7% Invest. Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" -msgstr "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" +msgstr "7.7%" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." -msgstr "8.0% Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." +msgstr "7.7% Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" -msgstr "8.0% Einkauf" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" +msgstr "7.7% Einkauf" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." -msgstr "8.0% Einkauf Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." +msgstr "7.7% Einkauf Inkl." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." -msgstr "8.0% Invest." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "7.7% Invest." #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." -msgstr "8.0% Invest. Inkl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." +msgstr "7.7% Invest. Inkl." #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4200 @@ -255,7 +241,8 @@ msgstr "Abschreibungen auf Fahrzeuge" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" -msgstr "Abschreibungen und Wertberichtigungen feste Einrichtungen und Installationen" +msgstr "" +"Abschreibungen und Wertberichtigungen feste Einrichtungen und Installationen" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2270 @@ -313,37 +300,6 @@ msgid "Avances et prêts" msgstr "Vorschüsse und Darlehen" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "Bank" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "Bankkonten" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "" -"Boolean option field indicating whether or not the alternate layout (the one " -"printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "" -"Boolean value telling whether or not the ISR corresponding to this invoice " -"has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "" -"Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "Patente, Know-how, Lizenzen, Rechte, Entwicklungen" @@ -359,11 +315,6 @@ msgid "Bénéfice / perte reporté" msgstr "Gewinnvortrag oder Verlustvortrag" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "CHF ESR Referenznummer" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "" "Capital actions, capital social, droits de participations ou capital de " @@ -521,11 +472,6 @@ msgid "Créanciers" msgstr "Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "Währung" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" @@ -578,11 +524,6 @@ msgid "Déductions sur ventes" msgstr "Erlösminderungen" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "Wasser" @@ -598,11 +539,6 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "Darlehen" @@ -677,34 +613,6 @@ msgid "Hypothèques" msgstr "Hypotheken" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "" -"ISR number split in blocks of 5 characters (right-justified), to generate " -"ISR report." -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "Geschäftsliegenschaften" @@ -752,51 +660,6 @@ msgid "Installations de stockage" msgstr "Installations de stockage" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "Fahrzeugleasing und -mieten" @@ -807,6 +670,11 @@ msgid "Leasing immobilisations corporelles meubles" msgstr "Leasingaufwand mobile Sachanlagen" #. module: l10n_ch +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "Büromaschinen, Informatik, Kommunikationstechnologie" @@ -852,11 +720,6 @@ msgid "Mobilier et installations" msgstr "Mobiliar und Einrichtungen" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "Werkzeuge und Geräte" @@ -882,13 +745,6 @@ msgid "Plan comptable 2015" msgstr "Kontenrahmen 2015" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "" -"Postal reference of the bank, formated with '-' and without the padding " -"zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "Aufwand für bezogene Dienstleistungen" @@ -899,30 +755,6 @@ msgid "Prestations propres" msgstr "Eigenleistungen" #. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "" -"Print the coordinates of your bank under the 'Payment for' title of the " -"ISR.\n" -" Your address will be moved to the 'in favour " -"of' section." -msgstr "" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "Ertrag Nebenbetrieb" @@ -1035,11 +867,6 @@ msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "CH Bilanz- und Erfolgsrechnungen" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1080,13 +907,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1100,13 +927,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1120,6 +947,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1145,6 +982,11 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "MwSt. 0%" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "MwSt. 0% auf Importe von Gütern und Dienstleistungen" @@ -1155,6 +997,11 @@ msgid "TVA 0% exclue" msgstr "MwSt. 0% ausgeschlossen" #. module: l10n_ch +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "MwSt. 2.5%" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "MwSt. 2,5 % auf Einkäufe B&S (inkl. USt)" @@ -1175,44 +1022,59 @@ msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "MwSt. 2,5% auf Invest. und sonst. Aufw. (USt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" -msgstr "MwSt. 3,8% auf Einkäufe B&S (inkl. LSt)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" +msgstr "MwSt. 3,7%" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" +msgstr "MwSt. 3,7% auf Einkäufe B&S (inkl. LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "MwSt. 3,8% auf Einkäufe B&S (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" +msgstr "MwSt. 3,7% auf Einkäufe B&S (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" -msgstr "MwSt. 3,8% auf Invest. und sonst. Aufw. (inkl. LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" +msgstr "MwSt. 3,7% auf Invest. und sonst. Aufw. (inkl. LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" -msgstr "MwSt. 3,8% auf Invest. und sonst. Aufw. (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" +msgstr "MwSt. 3,7% auf Invest. und sonst. Aufw. (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" -msgstr "MwSt. 8.0% auf Einkäufe B&S (inkl. NRZ)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" +msgstr "MwSt. 7,7%" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" -msgstr "MwSt. 8,0% auf Einkäufe B&S (NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" +msgstr "MwSt. 7,7% auf Einkäufe B&S (inkl. NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" -msgstr "MwSt. 8,0% auf Invest. und sonst. Aufw. (inkl. NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" +msgstr "MwSt. 7,7% auf Einkäufe B&S (NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" -msgstr "MwSt. 8,0% auf Invest. und sonst. Aufw. (NRZ)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" +msgstr "MwSt. 7,7% auf Invest. und sonst. Aufw. (inkl. NRZ)" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" +msgstr "MwSt. 7,7% auf Invest. und sonst. Aufw. (NRZ)" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_2200 @@ -1230,61 +1092,34 @@ msgid "TVA due a 2.5% (TR)" msgstr "Zu zahlende MwSt. bei 2,5% (USt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" -msgstr "Zu zahlende MwSt. bei 3,8% (LSt)" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" +msgstr "Zu zahlende MwSt. bei 3,7% (LSt)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "Zu zahlende MwSt. bei 8,0% (NRZ)" #. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" -msgstr "Zu zahlende MwSt. bei 2,5% (inkl. USt)" - -#. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" -msgstr "Zu zahlende MwSt. bei 3,8% (inkl. LSt)" - -#. module: l10n_ch -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" -msgstr "Zu zahlende MwSt. bei 8,0% (inkl. NRZ)" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in CHF." -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" +msgstr "Zu zahlende MwSt. bei 2,5% (inkl. USt)" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in EUR." -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" +msgstr "Zu zahlende MwSt. bei 3,7% (inkl. LSt)" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" -msgstr "" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" +msgstr "Zu zahlende MwSt. bei 7,7% (inkl. NRZ)" #. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1060 @@ -1297,11 +1132,6 @@ msgid "Titres à long terme" msgstr "Wertschriften" #. module: l10n_ch -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "Interne Verrechnungen" - -#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "Nicht fakturierte Dienstleistungen" @@ -1377,30 +1207,3 @@ msgstr "Produktionserlöse" #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "Fahrzeuge" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "" -"You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or " -"an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal " -"reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the " -"invoice with the postal to be used to receive the related payment. A default " -"account will be automatically set for all invoices created after you defined " -"a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" diff --git a/addons/l10n_ch/i18n_extra/it.po b/addons/l10n_ch/i18n_extra/it.po index 820569f207b..2af6ef38dc0 100644 --- a/addons/l10n_ch/i18n_extra/it.po +++ b/addons/l10n_ch/i18n_extra/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" "PO-Revision-Date: 2014-12-02 09:47+0000\n" "Last-Translator: <>\n" "Language-Team: \n" @@ -17,201 +17,173 @@ msgstr "" "Plural-Forms: \n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_100_import #: model:account.tax.template,name:l10n_ch.vat_100_import msgid "100% dédouanement TVA" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4200 #: model:account.account.template,name:l10n_ch.ch_coa_4200 msgid "Achats de marchandises destinées à la revente" msgstr "Costi delle merci di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2030 #: model:account.account.template,name:l10n_ch.ch_coa_2030 msgid "Acomptes de clients" msgstr "Acconti ricevuti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1208 #: model:account.account.template,name:l10n_ch.ch_coa_1208 msgid "Acomptes sur les marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1218 #: model:account.account.template,name:l10n_ch.ch_coa_1218 msgid "Acomptes sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1149 #: model:account.account.template,name:l10n_ch.ch_coa_1149 msgid "Ajustement de la valeur des avances et des prêts" msgstr "Rettifica valore anticipi e prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1199 #: model:account.account.template,name:l10n_ch.ch_coa_1199 msgid "Ajustement de la valeur des créances à court terme" msgstr "Rettifica valore crediti diversi a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1449 #: model:account.account.template,name:l10n_ch.ch_coa_1449 msgid "Ajustement de la valeur des créances à long terme" msgstr "Rettifica valore crediti a lungo termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1779 #: model:account.account.template,name:l10n_ch.ch_coa_1779 msgid "Ajustement de la valeur des goodwill" msgstr "Rettifica valore goodwill" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1489 #: model:account.account.template,name:l10n_ch.ch_coa_1489 msgid "Ajustement de la valeur des participations" msgstr "Rettifica valore partecipazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1069 -#: model:account.account,name:l10n_ch.1_ch_coa_1409 #: model:account.account.template,name:l10n_ch.ch_coa_1069 #: model:account.account.template,name:l10n_ch.ch_coa_1409 msgid "Ajustement de la valeur des titres" msgstr "Rettifica valore titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6800 #: model:account.account.template,name:l10n_ch.ch_coa_6800 msgid "" "Amortissements et ajustements de valeur des postes sur immobilisations " @@ -219,187 +191,126 @@ msgid "" msgstr "Ammortamenti e rettifiche di valore dell’attivo fisso" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1519 #: model:account.account.template,name:l10n_ch.ch_coa_1519 msgid "Amortissements sur le mobilier et les installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1599 #: model:account.account.template,name:l10n_ch.ch_coa_1599 msgid "Amortissements sur les autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1709 #: model:account.account.template,name:l10n_ch.ch_coa_1709 msgid "Amortissements sur les brevets, know-how, licences, droits, dév." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1609 #: model:account.account.template,name:l10n_ch.ch_coa_1609 msgid "Amortissements sur les immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1559 #: model:account.account.template,name:l10n_ch.ch_coa_1559 msgid "Amortissements sur les installations de stockage" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1529 #: model:account.account.template,name:l10n_ch.ch_coa_1529 msgid "Amortissements sur les machines de bureau, inf. et syst. comm." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1509 #: model:account.account.template,name:l10n_ch.ch_coa_1509 msgid "Amortissements sur les machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1549 #: model:account.account.template,name:l10n_ch.ch_coa_1549 msgid "Amortissements sur les outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1539 #: model:account.account.template,name:l10n_ch.ch_coa_1539 msgid "Amortissements sur les véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1579 #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2270 #: model:account.account.template,name:l10n_ch.ch_coa_2270 msgid "Assurances sociales et institutions de prévoyance" msgstr "Assicurazioni sociali e istituti di previdenza" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6300 #: model:account.account.template,name:l10n_ch.ch_coa_6300 msgid "Assurances-choses, droits, taxes, autorisations" msgstr "Assicurazioni cose, contributi, tasse e autorizzazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5800 #: model:account.account.template,name:l10n_ch.ch_coa_5800 msgid "Autres charges du personnel" msgstr "Altri costi del personale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6700 #: model:account.account.template,name:l10n_ch.ch_coa_6700 msgid "Autres charges d‘exploitation" msgstr "Altri costi d’esercizio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1190 #: model:account.account.template,name:l10n_ch.ch_coa_1190 msgid "Autres créances à court terme" msgstr "Altri crediti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2210 #: model:account.account.template,name:l10n_ch.ch_coa_2210 msgid "Autres dettes à court terme" msgstr "Altri debiti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2140 #: model:account.account.template,name:l10n_ch.ch_coa_2140 msgid "Autres dettes à court terme rémunérées" msgstr "Altri debiti onerosi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2500 #: model:account.account.template,name:l10n_ch.ch_coa_2500 msgid "Autres dettes à long terme" msgstr "Altri debiti a lungo termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1590 #: model:account.account.template,name:l10n_ch.ch_coa_1590 msgid "Autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3600 #: model:account.account.template,name:l10n_ch.ch_coa_3600 msgid "Autres ventes et prestations de services" msgstr "Altri ricavi e prestazioni di servizi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1140 #: model:account.account.template,name:l10n_ch.ch_coa_1140 msgid "Avances et prêts" msgstr "Anticipi e prestiti" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "" -"Boolean option field indicating whether or not the alternate layout (the one " -"printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "" -"Boolean value telling whether or not the ISR corresponding to this invoice " -"has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "" -"Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1700 #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "Patenti, know-how, licenze, diritti e sviluppo" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2979 #: model:account.account.template,name:l10n_ch.ch_coa_2979 msgid "Bénéfice / perte de l’exercice" msgstr "Utile/perdita annuale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2970 #: model:account.account.template,name:l10n_ch.ch_coa_2970 msgid "Bénéfice / perte reporté" msgstr "Utile / perdita riportata" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1850 #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "" "Capital actions, capital social, droits de participations ou capital de " @@ -409,7 +320,6 @@ msgstr "" "della fondazione non versati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2800 #: model:account.account.template,name:l10n_ch.ch_coa_2800 msgid "Capital-actions, capital social, capital de fondation" msgstr "" @@ -417,109 +327,91 @@ msgstr "" "della fondazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4521 #: model:account.account.template,name:l10n_ch.ch_coa_4521 msgid "Charbon, briquettes, bois" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7010 #: model:account.account.template,name:l10n_ch.ch_coa_7010 msgid "Charges accessoires" msgstr "Costi attività accessoria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6000 #: model:account.account.template,name:l10n_ch.ch_coa_6000 msgid "Charges de locaux" msgstr "Costi dei locali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4000 #: model:account.account.template,name:l10n_ch.ch_coa_4000 msgid "Charges de matériel de l‘atelier" msgstr "Costi materiale per la fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5900 #: model:account.account.template,name:l10n_ch.ch_coa_5900 msgid "Charges de personnels temporaires" msgstr "Prestazioni di terz" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6200 #: model:account.account.template,name:l10n_ch.ch_coa_6200 msgid "Charges de véhicules et de transport" msgstr "Costi auto e di trasporto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7510 #: model:account.account.template,name:l10n_ch.ch_coa_7510 msgid "Charges des immeubles d‘exploitation" msgstr "Costi immobili aziendali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6500 #: model:account.account.template,name:l10n_ch.ch_coa_6500 msgid "Charges d‘administration" msgstr "Costi amministrativi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6400 #: model:account.account.template,name:l10n_ch.ch_coa_6400 msgid "Charges d’énergie et évacuation des déchets" msgstr "Costi energia e smaltimento" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6570 #: model:account.account.template,name:l10n_ch.ch_coa_6570 msgid "Charges et leasing d’informatique" msgstr "Costi informatici incluso leasing" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8500 #: model:account.account.template,name:l10n_ch.ch_coa_8500 msgid "Charges extraordinaires, exceptionnelles ou hors période" msgstr "Costi straordinari, unici o relativi ad altri periodi contabili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6900 #: model:account.account.template,name:l10n_ch.ch_coa_6900 msgid "Charges financières" msgstr "Costi finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8000 #: model:account.account.template,name:l10n_ch.ch_coa_8000 msgid "Charges hors exploitation" msgstr "Costi estranei" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1300 #: model:account.account.template,name:l10n_ch.ch_coa_1300 msgid "Charges payées d‘avance" msgstr "Costi anticipati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5700 #: model:account.account.template,name:l10n_ch.ch_coa_5700 msgid "Charges sociales" msgstr "Oneri sociali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2300 #: model:account.account.template,name:l10n_ch.ch_coa_2300 msgid "Charges à payer" msgstr "Costi da pagare" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3803 #: model:account.account.template,name:l10n_ch.ch_coa_3803 msgid "Commissions de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4903 #: model:account.account.template,name:l10n_ch.ch_coa_4903 msgid "Commissions obtenues sur achats" msgstr "" @@ -530,147 +422,113 @@ msgid "Companies" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1099 #: model:account.account.template,name:l10n_ch.ch_coa_1099 msgid "Compte d'attente autre" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1091 #: model:account.account.template,name:l10n_ch.ch_coa_1091 msgid "Compte d'attente pour salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3710 #: model:account.account.template,name:l10n_ch.ch_coa_3710 msgid "Consommations propres" msgstr "Consumo proprio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1269 #: model:account.account.template,name:l10n_ch.ch_coa_1269 msgid "Correction de la valeur de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1279 #: model:account.account.template,name:l10n_ch.ch_coa_1279 msgid "Corrections de la valeur des stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1209 #: model:account.account.template,name:l10n_ch.ch_coa_1209 msgid "Corrections de la valeur des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1289 #: model:account.account.template,name:l10n_ch.ch_coa_1289 msgid "Corrections de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1219 #: model:account.account.template,name:l10n_ch.ch_coa_1219 msgid "Corrections de la valeur sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1180 #: model:account.account.template,name:l10n_ch.ch_coa_1180 msgid "Créances envers les assurances sociales et institutions de prévoyance" msgstr "Crediti da assicurazioni sociali e istituti di previdenza" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2000 #: model:account.account.template,name:l10n_ch.ch_coa_2000 msgid "Créanciers" msgstr "Debiti per forniture e prestazioni (creditori)" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2100 -#: model:account.account,name:l10n_ch.1_ch_coa_2400 #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" msgstr "Banca passiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2160 #: model:account.account.template,name:l10n_ch.ch_coa_2160 msgid "Dettes envers l'actionnaire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3806 -#: model:account.account,name:l10n_ch.1_ch_coa_4906 #: model:account.account.template,name:l10n_ch.ch_coa_3806 #: model:account.account.template,name:l10n_ch.ch_coa_4906 msgid "Différences de change" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2261 #: model:account.account.template,name:l10n_ch.ch_coa_2261 msgid "Dividendes" msgstr "Dividendi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4071 #: model:account.account.template,name:l10n_ch.ch_coa_4071 msgid "Droits de douanes à l'importation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1109 #: model:account.account.template,name:l10n_ch.ch_coa_1109 msgid "Ducroire" msgstr "Delcredere" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1100 #: model:account.account.template,name:l10n_ch.ch_coa_1100 msgid "Débiteurs" msgstr "Crediti da forniture e prestazioni (debitori)" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2201 #: model:account.account.template,name:l10n_ch.ch_coa_2201 msgid "Décompte TVA" msgstr "IVA, rendiconto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4009 #: model:account.account.template,name:l10n_ch.ch_coa_4009 msgid "Déductions obtenues sur achats" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3009 #: model:account.account.template,name:l10n_ch.ch_coa_3009 msgid "Déductions sur ventes" msgstr "Diminuzione di ricavi" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4540 #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4500 #: model:account.account.template,name:l10n_ch.ch_coa_4500 msgid "Electricité" msgstr "" @@ -681,323 +539,202 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2450 #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "Prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2430 #: model:account.account.template,name:l10n_ch.ch_coa_2430 msgid "Emprunts obligataires" msgstr "Prestiti obbligazionari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2120 -#: model:account.account,name:l10n_ch.1_ch_coa_2420 #: model:account.account.template,name:l10n_ch.ch_coa_2120 #: model:account.account.template,name:l10n_ch.ch_coa_2420 msgid "Engagements de financement par leasing" msgstr "Impegni leasing finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6100 #: model:account.account.template,name:l10n_ch.ch_coa_6100 msgid "" "Entretien, réparations et remplacement des inst. servant à l’exploitation" msgstr "Manutenzioni, riparazioni e sostituzione immobilizzazioni mobiliari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1570 #: model:account.account.template,name:l10n_ch.ch_coa_1570 msgid "Equipements et Installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3800 -#: model:account.account,name:l10n_ch.1_ch_coa_4900 #: model:account.account.template,name:l10n_ch.ch_coa_3800 #: model:account.account.template,name:l10n_ch.ch_coa_4900 msgid "Escomptes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4530 #: model:account.account.template,name:l10n_ch.ch_coa_4530 msgid "Essence" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3804 #: model:account.account.template,name:l10n_ch.ch_coa_3804 msgid "Frais d'encaissement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3807 #: model:account.account.template,name:l10n_ch.ch_coa_3807 msgid "Frais d'expédition" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4072 #: model:account.account.template,name:l10n_ch.ch_coa_4072 msgid "Frais de transport à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4070 #: model:account.account.template,name:l10n_ch.ch_coa_4070 msgid "Frêts à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4510 #: model:account.account.template,name:l10n_ch.ch_coa_4510 msgid "Gaz" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1770 #: model:account.account.template,name:l10n_ch.ch_coa_1770 msgid "Goodwill" msgstr "Goodwill" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1441 -#: model:account.account,name:l10n_ch.1_ch_coa_2451 #: model:account.account.template,name:l10n_ch.ch_coa_1441 #: model:account.account.template,name:l10n_ch.ch_coa_2451 msgid "Hypothèques" msgstr "Prestiti ipotecari" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "" -"ISR number split in blocks of 5 characters (right-justified), to generate " -"ISR report." -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1600 #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "Immobili aziendali" #. module: l10n_ch -#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_import #: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_import msgid "Import/Export" msgstr "Import/Export" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1176 #: model:account.account.template,name:l10n_ch.ch_coa_1176 msgid "Impôt anticipé" msgstr "Imposta preventiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2206 #: model:account.account.template,name:l10n_ch.ch_coa_2206 msgid "Impôt anticipé dû" msgstr "Imposta preventiva" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1171 #: model:account.account.template,name:l10n_ch.ch_coa_1171 msgid "Impôt préalable: TVA s/investissements et autres charges d’exploitation" msgstr "IVA, imposta precedente su investimenti e altri costi d’esercizio" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1170 #: model:account.account.template,name:l10n_ch.ch_coa_1170 msgid "Impôt préalable: TVA s/matériel, marchandises, prestations et énergie" msgstr "IVA, Imposta precedente su materiale, merce, servizi e energia" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1189 -#: model:account.account,name:l10n_ch.1_ch_coa_2279 #: model:account.account.template,name:l10n_ch.ch_coa_1189 #: model:account.account.template,name:l10n_ch.ch_coa_2279 msgid "Impôt à la source" msgstr "Imposte alla fonte" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2208 -#: model:account.account,name:l10n_ch.1_ch_coa_8900 #: model:account.account.template,name:l10n_ch.ch_coa_2208 #: model:account.account.template,name:l10n_ch.ch_coa_8900 msgid "Impôts directs" msgstr "Imposte dirette" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1550 #: model:account.account.template,name:l10n_ch.ch_coa_1550 msgid "Installations de stockage" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6260 #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "Leasing e noleggio auto" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6105 #: model:account.account.template,name:l10n_ch.ch_coa_6105 msgid "Leasing immobilisations corporelles meubles" msgstr "Costi leasing immobilizzazioni mobiliari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1520 +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "Macchine ufficio, informatica e tecnologia della comunicazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1500 #: model:account.account.template,name:l10n_ch.ch_coa_1500 msgid "Machines et appareils" msgstr "Macchine e attrezzature" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1200 #: model:account.account.template,name:l10n_ch.ch_coa_1200 msgid "Marchandises commerciales" msgstr "Merce di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1250 #: model:account.account.template,name:l10n_ch.ch_coa_1250 msgid "Marchandises en consignation" msgstr "Merce in consegna" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1220 #: model:account.account.template,name:l10n_ch.ch_coa_1220 msgid "Matières auxiliaires" msgstr "Materia ausiliaria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1230 #: model:account.account.template,name:l10n_ch.ch_coa_1230 msgid "Matières consommables" msgstr "Materiale di consumo" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1210 #: model:account.account.template,name:l10n_ch.ch_coa_1210 msgid "Matières premières" msgstr "Materia prima" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4520 #: model:account.account.template,name:l10n_ch.ch_coa_4520 msgid "Mazout" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1510 #: model:account.account.template,name:l10n_ch.ch_coa_1510 msgid "Mobilier et installations" msgstr "Mobilio e installazioni" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1540 #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "Utensili e apparecchiature" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1480 #: model:account.account.template,name:l10n_ch.ch_coa_1480 msgid "Participations" msgstr "Partecipazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4086 #: model:account.account.template,name:l10n_ch.ch_coa_4086 msgid "Pertes de matières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3805 #: model:account.account.template,name:l10n_ch.ch_coa_3805 msgid "Pertes sur créances clients, variation ducroire" msgstr "" @@ -1008,92 +745,51 @@ msgid "Plan comptable 2015" msgstr "Piano Conti 2015" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "" -"Postal reference of the bank, formated with '-' and without the padding " -"zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4400 #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "Lavori di terzi / prestazioni di subappaltanti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3700 #: model:account.account.template,name:l10n_ch.ch_coa_3700 msgid "Prestations propres" msgstr "Lavori interni" #. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "" -"Print the coordinates of your bank under the 'Payment for' title of the " -"ISR.\n" -" Your address will be moved to the 'in favour " -"of' section." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7000 #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "Ricavi attività accessoria" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7500 #: model:account.account.template,name:l10n_ch.ch_coa_7500 msgid "Produits des immeubles d‘exploitation" msgstr "Ricavi immobili aziendali" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2301 #: model:account.account.template,name:l10n_ch.ch_coa_2301 msgid "Produits encaissés d’avance" msgstr "Ricavi incassati dell’anno seguente" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8510 #: model:account.account.template,name:l10n_ch.ch_coa_8510 msgid "Produits extraordinaires, exceptionnels ou hors période" msgstr "Ricavi straordinari, unici o relativi ad altri periodi contabili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6950 #: model:account.account.template,name:l10n_ch.ch_coa_6950 msgid "Produits financiers" msgstr "Ricavi finanziari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8100 #: model:account.account.template,name:l10n_ch.ch_coa_8100 msgid "Produits hors exploitation" msgstr "Ricavi estranei" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1301 #: model:account.account.template,name:l10n_ch.ch_coa_1301 msgid "Produits à recevoir" msgstr "Ricavi da incassare" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2980 #: model:account.account.template,name:l10n_ch.ch_coa_2980 msgid "" "Propres actions, parts sociales, droits de participations (poste négatif)" @@ -1101,97 +797,77 @@ msgstr "" "Azioni proprie, parti sociali, diritti di partecipazione (posta negativa)" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2600 #: model:account.account.template,name:l10n_ch.ch_coa_2600 msgid "Provisions" msgstr "Accantonamenti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2330 #: model:account.account.template,name:l10n_ch.ch_coa_2330 msgid "Provisions à court terme" msgstr "Accantonamenti a breve termine" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1440 #: model:account.account.template,name:l10n_ch.ch_coa_1440 msgid "Prêts" msgstr "Prestiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6600 #: model:account.account.template,name:l10n_ch.ch_coa_6600 msgid "Publicité" msgstr "Costi pubblicitari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3801 #: model:account.account.template,name:l10n_ch.ch_coa_3801 msgid "Rabais et réduction de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4901 #: model:account.account.template,name:l10n_ch.ch_coa_4901 msgid "Rabais et réductions de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3802 -#: model:account.account,name:l10n_ch.1_ch_coa_4092 #: model:account.account.template,name:l10n_ch.ch_coa_3802 #: model:account.account.template,name:l10n_ch.ch_coa_4092 msgid "Ristournes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2940 #: model:account.account.template,name:l10n_ch.ch_coa_2940 msgid "Réserves d‘évaluation" msgstr "Riserve da rivalutazioni" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2960 #: model:account.account.template,name:l10n_ch.ch_coa_2960 msgid "Réserves libres" msgstr "Riserve facoltative da utili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2950 #: model:account.account.template,name:l10n_ch.ch_coa_2950 msgid "Réserves légales issues du bénéfice" msgstr "Riserva legale da utili" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2900 #: model:account.account.template,name:l10n_ch.ch_coa_2900 msgid "Réserves légales issues du capital" msgstr "Riserva legale da capitale" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5000 #: model:account.account.template,name:l10n_ch.ch_coa_5000 msgid "Salaires" msgstr "Salari" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1260 #: model:account.account.template,name:l10n_ch.ch_coa_1260 msgid "Stocks de produits finis" msgstr "Prodotti finiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1270 #: model:account.account.template,name:l10n_ch.ch_coa_1270 msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1232,13 +908,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1252,13 +928,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1272,6 +948,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1297,304 +983,228 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_import +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "IVA 0% Importazioni di bene e servizi" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_exclude #: model:account.tax.template,name:l10n_ch.vat_O_exclude msgid "TVA 0% exclue" msgstr "IVA 0% Esclusa" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase_incl +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase #: model:account.tax.template,name:l10n_ch.vat_25_purchase msgid "TVA 2.5% sur achat B&S (TR)" msgstr "TVA 2.5% sur achat B&S (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,name:l10n_ch.vat_25_invest_incl msgid "TVA 2.5% sur invest. et autres ch. (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest #: model:account.tax.template,name:l10n_ch.vat_25_invest msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "IVA 2.5% Investimenti e altri costi (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "TVA 3.8% sur achat B&S (TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" -msgstr "IVA 3.8% Investimenti e altri costi (TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" -msgstr "TVA 8.0% sur achat B&S (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" +msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" -msgstr "IVA 8.0% Investimenti e altri costi (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" +msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2200 #: model:account.account.template,name:l10n_ch.ch_coa_2200 msgid "TVA due" msgstr "IVA dovuta" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_XO #: model:account.tax.template,name:l10n_ch.vat_XO msgid "TVA due a 0% (Exportations)" msgstr "IVA Vendite 0% (Export)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25 #: model:account.tax.template,name:l10n_ch.vat_25 msgid "TVA due a 2.5% (TR)" msgstr "IVA Vendite al 2.5% (TR)" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38 -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" -msgstr "IVA Vendite al 3.8% (TS)" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80 -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" -msgstr "IVA Vendite al 8.0% (TN)" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_incl -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_incl -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_incl -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in CHF." +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "" -"The postal reference of the bank, used to generate ISR payment slips in EUR." +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1060 #: model:account.account.template,name:l10n_ch.ch_coa_1060 msgid "Titres" msgstr "Titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1400 #: model:account.account.template,name:l10n_ch.ch_coa_1400 msgid "Titres à long terme" msgstr "Titoli" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_transfer_account_id -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1280 #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "Prodotti in corso di fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3940 #: model:account.account.template,name:l10n_ch.ch_coa_3940 msgid "Variation de la valeur des prestations non facturées" msgstr "Variazione prestazioni di servizi non fatturate" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1287 #: model:account.account.template,name:l10n_ch.ch_coa_1287 msgid "Variation de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1277 #: model:account.account.template,name:l10n_ch.ch_coa_1277 msgid "Variation de stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1267 #: model:account.account.template,name:l10n_ch.ch_coa_1267 msgid "Variation de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1207 -#: model:account.account,name:l10n_ch.1_ch_coa_4800 #: model:account.account.template,name:l10n_ch.ch_coa_1207 #: model:account.account.template,name:l10n_ch.ch_coa_4800 msgid "Variation des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4801 #: model:account.account.template,name:l10n_ch.ch_coa_4801 msgid "Variation des stocks de matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3901 #: model:account.account.template,name:l10n_ch.ch_coa_3901 msgid "Variation des stocks de produits finis" msgstr "Variazione delle scorte di prodotti finiti" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3900 #: model:account.account.template,name:l10n_ch.ch_coa_3900 msgid "Variation des stocks de produits semi-finis" msgstr "Variazione delle scorte di prodotti in corso di fabbricazione" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1217 #: model:account.account.template,name:l10n_ch.ch_coa_1217 msgid "Variation des stocks des matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4008 -#: model:account.account,name:l10n_ch.1_ch_coa_4080 #: model:account.account.template,name:l10n_ch.ch_coa_4008 #: model:account.account.template,name:l10n_ch.ch_coa_4080 msgid "Variations de stocks" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3200 #: model:account.account.template,name:l10n_ch.ch_coa_3200 msgid "Ventes de marchandises" msgstr "Ricavi merci di rivendita" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3400 #: model:account.account.template,name:l10n_ch.ch_coa_3400 msgid "Ventes de prestations" msgstr "Ricavi prestazioni di servizi" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3000 #: model:account.account.template,name:l10n_ch.ch_coa_3000 msgid "Ventes de produits fabriqués" msgstr "Ricavi prodotti fabbricati" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1530 #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "Veicoli" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "" -"You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or " -"an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal " -"reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the " -"invoice with the postal to be used to receive the related payment. A default " -"account will be automatically set for all invoices created after you defined " -"a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" diff --git a/addons/l10n_ch/i18n_extra/l10n_ch.pot b/addons/l10n_ch/i18n_extra/l10n_ch.pot index e2c03bcba8b..73b91c2b356 100644 --- a/addons/l10n_ch/i18n_extra/l10n_ch.pot +++ b/addons/l10n_ch/i18n_extra/l10n_ch.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.saas~15+e\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-14 15:52+0000\n" -"PO-Revision-Date: 2017-03-14 15:52+0000\n" +"POT-Creation-Date: 2019-02-12 05:39+0000\n" +"PO-Revision-Date: 2019-02-12 05:39+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,496 +16,393 @@ msgstr "" "Plural-Forms: \n" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_exclude #: model:account.tax.template,description:l10n_ch.vat_O_exclude msgid "0% excl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_O_import #: model:account.tax.template,description:l10n_ch.vat_O_import msgid "0% import." msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_100_import #: model:account.tax.template,name:l10n_ch.vat_100_import msgid "100% dédouanement TVA" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_100_import #: model:account.tax.template,description:l10n_ch.vat_100_import msgid "100% imp." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25 #: model:account.tax.template,description:l10n_ch.vat_25 msgid "2.5%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_incl #: model:account.tax.template,description:l10n_ch.vat_25_incl msgid "2.5% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase #: model:account.tax.template,description:l10n_ch.vat_25_purchase msgid "2.5% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_purchase_incl #: model:account.tax.template,description:l10n_ch.vat_25_purchase_incl msgid "2.5% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest #: model:account.tax.template,description:l10n_ch.vat_25_invest msgid "2.5% invest." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,description:l10n_ch.vat_25_invest_incl msgid "2.5% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38 -#: model:account.tax.template,description:l10n_ch.vat_38 -msgid "3.8%" +#: model:account.tax.template,description:l10n_ch.vat_37 +msgid "3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_incl -#: model:account.tax.template,description:l10n_ch.vat_38_incl -msgid "3.8% Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_incl +msgid "3.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,description:l10n_ch.vat_38_purchase -msgid "3.8% achat" +#: model:account.tax.template,description:l10n_ch.vat_37_purchase +msgid "3.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_38_purchase_incl -msgid "3.8% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_purchase_incl +msgid "3.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest -#: model:account.tax.template,description:l10n_ch.vat_38_invest -msgid "3.8% invest" +#: model:account.tax.template,description:l10n_ch.vat_37_invest +msgid "3.7% invest" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_38_invest_incl -msgid "3.8% invest Incl." +#: model:account.tax.template,description:l10n_ch.vat_37_invest_incl +msgid "3.7% invest Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80 -#: model:account.tax.template,description:l10n_ch.vat_80 -msgid "8.0%" +#: model:account.tax.template,description:l10n_ch.vat_77 +msgid "7.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_incl -#: model:account.tax.template,description:l10n_ch.vat_80_incl -msgid "8.0% Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_incl +msgid "7.7% Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,description:l10n_ch.vat_80_purchase -msgid "8.0% achat" +#: model:account.tax.template,description:l10n_ch.vat_77_purchase +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_reverse +msgid "7.7% achat" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,description:l10n_ch.vat_80_purchase_incl -msgid "8.0% achat Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_return +msgid "7.7% achat (return)" msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest -#: model:account.tax.template,description:l10n_ch.vat_80_invest -msgid "8.0% invest." +#: model:account.tax.template,description:l10n_ch.vat_77_purchase_incl +msgid "7.7% achat Incl." msgstr "" #. module: l10n_ch -#: model:account.tax,description:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,description:l10n_ch.vat_80_invest_incl -msgid "8.0% invest. Incl." +#: model:account.tax.template,description:l10n_ch.vat_77_invest +msgid "7.7% invest." +msgstr "" + +#. module: l10n_ch +#: model:account.tax.template,description:l10n_ch.vat_77_invest_incl +msgid "7.7% invest. Incl." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4200 #: model:account.account.template,name:l10n_ch.ch_coa_4200 msgid "Achats de marchandises destinées à la revente" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2030 #: model:account.account.template,name:l10n_ch.ch_coa_2030 msgid "Acomptes de clients" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1208 #: model:account.account.template,name:l10n_ch.ch_coa_1208 msgid "Acomptes sur les marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1218 #: model:account.account.template,name:l10n_ch.ch_coa_1218 msgid "Acomptes sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1149 #: model:account.account.template,name:l10n_ch.ch_coa_1149 msgid "Ajustement de la valeur des avances et des prêts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1199 #: model:account.account.template,name:l10n_ch.ch_coa_1199 msgid "Ajustement de la valeur des créances à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1449 #: model:account.account.template,name:l10n_ch.ch_coa_1449 msgid "Ajustement de la valeur des créances à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1779 #: model:account.account.template,name:l10n_ch.ch_coa_1779 msgid "Ajustement de la valeur des goodwill" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1489 #: model:account.account.template,name:l10n_ch.ch_coa_1489 msgid "Ajustement de la valeur des participations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1069 -#: model:account.account,name:l10n_ch.1_ch_coa_1409 #: model:account.account.template,name:l10n_ch.ch_coa_1069 #: model:account.account.template,name:l10n_ch.ch_coa_1409 msgid "Ajustement de la valeur des titres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6800 #: model:account.account.template,name:l10n_ch.ch_coa_6800 msgid "Amortissements et ajustements de valeur des postes sur immobilisations corporelles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1519 #: model:account.account.template,name:l10n_ch.ch_coa_1519 msgid "Amortissements sur le mobilier et les installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1599 #: model:account.account.template,name:l10n_ch.ch_coa_1599 msgid "Amortissements sur les autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1709 #: model:account.account.template,name:l10n_ch.ch_coa_1709 msgid "Amortissements sur les brevets, know-how, licences, droits, dév." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1609 #: model:account.account.template,name:l10n_ch.ch_coa_1609 msgid "Amortissements sur les immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1559 #: model:account.account.template,name:l10n_ch.ch_coa_1559 msgid "Amortissements sur les installations de stockage" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1529 #: model:account.account.template,name:l10n_ch.ch_coa_1529 msgid "Amortissements sur les machines de bureau, inf. et syst. comm." msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1509 #: model:account.account.template,name:l10n_ch.ch_coa_1509 msgid "Amortissements sur les machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1549 #: model:account.account.template,name:l10n_ch.ch_coa_1549 msgid "Amortissements sur les outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1539 #: model:account.account.template,name:l10n_ch.ch_coa_1539 msgid "Amortissements sur les véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1579 #: model:account.account.template,name:l10n_ch.ch_coa_1579 msgid "Amortissements sur les équipements et installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2270 #: model:account.account.template,name:l10n_ch.ch_coa_2270 msgid "Assurances sociales et institutions de prévoyance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6300 #: model:account.account.template,name:l10n_ch.ch_coa_6300 msgid "Assurances-choses, droits, taxes, autorisations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5800 #: model:account.account.template,name:l10n_ch.ch_coa_5800 msgid "Autres charges du personnel" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6700 #: model:account.account.template,name:l10n_ch.ch_coa_6700 msgid "Autres charges d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1190 #: model:account.account.template,name:l10n_ch.ch_coa_1190 msgid "Autres créances à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2210 #: model:account.account.template,name:l10n_ch.ch_coa_2210 msgid "Autres dettes à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2140 #: model:account.account.template,name:l10n_ch.ch_coa_2140 msgid "Autres dettes à court terme rémunérées" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2500 #: model:account.account.template,name:l10n_ch.ch_coa_2500 msgid "Autres dettes à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1590 #: model:account.account.template,name:l10n_ch.ch_coa_1590 msgid "Autres immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3600 #: model:account.account.template,name:l10n_ch.ch_coa_3600 msgid "Autres ventes et prestations de services" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1140 #: model:account.account.template,name:l10n_ch.ch_coa_1140 msgid "Avances et prêts" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_bank -msgid "Bank" -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_res_partner_bank -msgid "Bank Accounts" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -#: model:ir.model.fields,help:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "Boolean value telling whether or not the ISR corresponding to this invoice has already been printed or sent by mail." -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "Boolean value. True iff all the data required to generate the ISR are present" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1700 #: model:account.account.template,name:l10n_ch.ch_coa_1700 msgid "Brevets, know-how, licences, droits, développement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2979 #: model:account.account.template,name:l10n_ch.ch_coa_2979 msgid "Bénéfice / perte de l’exercice" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2970 #: model:account.account.template,name:l10n_ch.ch_coa_2970 msgid "Bénéfice / perte reporté" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "CHF ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1850 #: model:account.account.template,name:l10n_ch.ch_coa_1850 msgid "Capital actions, capital social, droits de participations ou capital de fondation non versés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2800 #: model:account.account.template,name:l10n_ch.ch_coa_2800 msgid "Capital-actions, capital social, capital de fondation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4521 #: model:account.account.template,name:l10n_ch.ch_coa_4521 msgid "Charbon, briquettes, bois" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7010 #: model:account.account.template,name:l10n_ch.ch_coa_7010 msgid "Charges accessoires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6000 #: model:account.account.template,name:l10n_ch.ch_coa_6000 msgid "Charges de locaux" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4000 #: model:account.account.template,name:l10n_ch.ch_coa_4000 msgid "Charges de matériel de l‘atelier" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5900 #: model:account.account.template,name:l10n_ch.ch_coa_5900 msgid "Charges de personnels temporaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6200 #: model:account.account.template,name:l10n_ch.ch_coa_6200 msgid "Charges de véhicules et de transport" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7510 #: model:account.account.template,name:l10n_ch.ch_coa_7510 msgid "Charges des immeubles d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6500 #: model:account.account.template,name:l10n_ch.ch_coa_6500 msgid "Charges d‘administration" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6400 #: model:account.account.template,name:l10n_ch.ch_coa_6400 msgid "Charges d’énergie et évacuation des déchets" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6570 #: model:account.account.template,name:l10n_ch.ch_coa_6570 msgid "Charges et leasing d’informatique" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8500 #: model:account.account.template,name:l10n_ch.ch_coa_8500 msgid "Charges extraordinaires, exceptionnelles ou hors période" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6900 #: model:account.account.template,name:l10n_ch.ch_coa_6900 msgid "Charges financières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8000 #: model:account.account.template,name:l10n_ch.ch_coa_8000 msgid "Charges hors exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1300 #: model:account.account.template,name:l10n_ch.ch_coa_1300 msgid "Charges payées d‘avance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5700 #: model:account.account.template,name:l10n_ch.ch_coa_5700 msgid "Charges sociales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2300 #: model:account.account.template,name:l10n_ch.ch_coa_2300 msgid "Charges à payer" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3803 #: model:account.account.template,name:l10n_ch.ch_coa_3803 msgid "Commissions de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4903 #: model:account.account.template,name:l10n_ch.ch_coa_4903 msgid "Commissions obtenues sur achats" msgstr "" @@ -516,147 +413,113 @@ msgid "Companies" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1099 #: model:account.account.template,name:l10n_ch.ch_coa_1099 msgid "Compte d'attente autre" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1091 #: model:account.account.template,name:l10n_ch.ch_coa_1091 msgid "Compte d'attente pour salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3710 #: model:account.account.template,name:l10n_ch.ch_coa_3710 msgid "Consommations propres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1269 #: model:account.account.template,name:l10n_ch.ch_coa_1269 msgid "Correction de la valeur de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1279 #: model:account.account.template,name:l10n_ch.ch_coa_1279 msgid "Corrections de la valeur des stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1209 #: model:account.account.template,name:l10n_ch.ch_coa_1209 msgid "Corrections de la valeur des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1289 #: model:account.account.template,name:l10n_ch.ch_coa_1289 msgid "Corrections de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1219 #: model:account.account.template,name:l10n_ch.ch_coa_1219 msgid "Corrections de la valeur sur matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1180 #: model:account.account.template,name:l10n_ch.ch_coa_1180 msgid "Créances envers les assurances sociales et institutions de prévoyance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2000 #: model:account.account.template,name:l10n_ch.ch_coa_2000 msgid "Créanciers" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "Currency" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2100 -#: model:account.account,name:l10n_ch.1_ch_coa_2400 #: model:account.account.template,name:l10n_ch.ch_coa_2100 #: model:account.account.template,name:l10n_ch.ch_coa_2400 msgid "Dettes bancaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2160 #: model:account.account.template,name:l10n_ch.ch_coa_2160 msgid "Dettes envers l'actionnaire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3806 -#: model:account.account,name:l10n_ch.1_ch_coa_4906 #: model:account.account.template,name:l10n_ch.ch_coa_3806 #: model:account.account.template,name:l10n_ch.ch_coa_4906 msgid "Différences de change" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2261 #: model:account.account.template,name:l10n_ch.ch_coa_2261 msgid "Dividendes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4071 #: model:account.account.template,name:l10n_ch.ch_coa_4071 msgid "Droits de douanes à l'importation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1109 #: model:account.account.template,name:l10n_ch.ch_coa_1109 msgid "Ducroire" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1100 #: model:account.account.template,name:l10n_ch.ch_coa_1100 msgid "Débiteurs" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2201 #: model:account.account.template,name:l10n_ch.ch_coa_2201 msgid "Décompte TVA" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4009 #: model:account.account.template,name:l10n_ch.ch_coa_4009 msgid "Déductions obtenues sur achats" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3009 #: model:account.account.template,name:l10n_ch.ch_coa_3009 msgid "Déductions sur ventes" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "EUR ISR reference" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4540 #: model:account.account.template,name:l10n_ch.ch_coa_4540 msgid "Eau" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4500 #: model:account.account.template,name:l10n_ch.ch_coa_4500 msgid "Electricité" msgstr "" @@ -667,320 +530,201 @@ msgid "Email Templates" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_mail_compose_message -msgid "Email composition wizard" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2450 #: model:account.account.template,name:l10n_ch.ch_coa_2450 msgid "Emprunts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2430 #: model:account.account.template,name:l10n_ch.ch_coa_2430 msgid "Emprunts obligataires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2120 -#: model:account.account,name:l10n_ch.1_ch_coa_2420 #: model:account.account.template,name:l10n_ch.ch_coa_2120 #: model:account.account.template,name:l10n_ch.ch_coa_2420 msgid "Engagements de financement par leasing" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6100 #: model:account.account.template,name:l10n_ch.ch_coa_6100 msgid "Entretien, réparations et remplacement des inst. servant à l’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1570 #: model:account.account.template,name:l10n_ch.ch_coa_1570 msgid "Equipements et Installations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3800 -#: model:account.account,name:l10n_ch.1_ch_coa_4900 #: model:account.account.template,name:l10n_ch.ch_coa_3800 #: model:account.account.template,name:l10n_ch.ch_coa_4900 msgid "Escomptes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4530 #: model:account.account.template,name:l10n_ch.ch_coa_4530 msgid "Essence" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3804 #: model:account.account.template,name:l10n_ch.ch_coa_3804 msgid "Frais d'encaissement" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3807 #: model:account.account.template,name:l10n_ch.ch_coa_3807 msgid "Frais d'expédition" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4072 #: model:account.account.template,name:l10n_ch.ch_coa_4072 msgid "Frais de transport à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4070 #: model:account.account.template,name:l10n_ch.ch_coa_4070 msgid "Frêts à l'achat" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4510 #: model:account.account.template,name:l10n_ch.ch_coa_4510 msgid "Gaz" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1770 #: model:account.account.template,name:l10n_ch.ch_coa_1770 msgid "Goodwill" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1441 -#: model:account.account,name:l10n_ch.1_ch_coa_2451 #: model:account.account.template,name:l10n_ch.ch_coa_1441 #: model:account.account.template,name:l10n_ch.ch_coa_2451 msgid "Hypothèques" msgstr "" #. module: l10n_ch -#: model:ir.actions.report.xml,name:l10n_ch.l10n_ch_isr_report -msgid "ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.l10n_ch_isr_report_template -msgid "ISR for invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "ISR number split in blocks of 5 characters (right-justified), to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.isr_invoice_search_view -msgid "ISR reference number" -msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/mail_compose_message.py:23 -#, python-format -msgid "ISR sent" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1600 #: model:account.account.template,name:l10n_ch.ch_coa_1600 msgid "Immeubles d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_import #: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_import msgid "Import/Export" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1176 #: model:account.account.template,name:l10n_ch.ch_coa_1176 msgid "Impôt anticipé" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2206 #: model:account.account.template,name:l10n_ch.ch_coa_2206 msgid "Impôt anticipé dû" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1171 #: model:account.account.template,name:l10n_ch.ch_coa_1171 msgid "Impôt préalable: TVA s/investissements et autres charges d’exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1170 #: model:account.account.template,name:l10n_ch.ch_coa_1170 msgid "Impôt préalable: TVA s/matériel, marchandises, prestations et énergie" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1189 -#: model:account.account,name:l10n_ch.1_ch_coa_2279 #: model:account.account.template,name:l10n_ch.ch_coa_1189 #: model:account.account.template,name:l10n_ch.ch_coa_2279 msgid "Impôt à la source" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2208 -#: model:account.account,name:l10n_ch.1_ch_coa_8900 #: model:account.account.template,name:l10n_ch.ch_coa_2208 #: model:account.account.template,name:l10n_ch.ch_coa_8900 msgid "Impôts directs" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1550 #: model:account.account.template,name:l10n_ch.ch_coa_1550 msgid "Installations de stockage" msgstr "" #. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_invoice -msgid "Invoice" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "L10N Ch Isr Number" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_number_spaced -msgid "L10N Ch Isr Number Spaced" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "L10N Ch Isr Optical Line" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "L10N Ch Isr Postal" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "L10N Ch Isr Postal Formatted" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_sent -msgid "L10N Ch Isr Sent" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_invoice_l10n_ch_isr_valid -msgid "L10N Ch Isr Valid" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "L10N Ch Postal" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6260 #: model:account.account.template,name:l10n_ch.ch_coa_6260 msgid "Leasing et location de véhicules" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6105 #: model:account.account.template,name:l10n_ch.ch_coa_6105 msgid "Leasing immobilisations corporelles meubles" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1520 +#: model:account.account.template,name:l10n_ch.l10nch_chart_template_liquidity_transfer +msgid "Liquidity Transfer" +msgstr "" + +#. module: l10n_ch #: model:account.account.template,name:l10n_ch.ch_coa_1520 msgid "Machines de bureau, informatique, systèmes de communication" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1500 #: model:account.account.template,name:l10n_ch.ch_coa_1500 msgid "Machines et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1200 #: model:account.account.template,name:l10n_ch.ch_coa_1200 msgid "Marchandises commerciales" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1250 #: model:account.account.template,name:l10n_ch.ch_coa_1250 msgid "Marchandises en consignation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1220 #: model:account.account.template,name:l10n_ch.ch_coa_1220 msgid "Matières auxiliaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1230 #: model:account.account.template,name:l10n_ch.ch_coa_1230 msgid "Matières consommables" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1210 #: model:account.account.template,name:l10n_ch.ch_coa_1210 msgid "Matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4520 #: model:account.account.template,name:l10n_ch.ch_coa_4520 msgid "Mazout" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1510 #: model:account.account.template,name:l10n_ch.ch_coa_1510 msgid "Mobilier et installations" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_optical_line -msgid "Optical reading line, as it will be printed on ISR" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1540 #: model:account.account.template,name:l10n_ch.ch_coa_1540 msgid "Outillages et appareils" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1480 #: model:account.account.template,name:l10n_ch.ch_coa_1480 msgid "Participations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4086 #: model:account.account.template,name:l10n_ch.ch_coa_4086 msgid "Pertes de matières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3805 #: model:account.account.template,name:l10n_ch.ch_coa_3805 msgid "Pertes sur créances clients, variation ducroire" msgstr "" @@ -991,183 +735,127 @@ msgid "Plan comptable 2015" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal_formatted -msgid "Postal reference of the bank, formated with '-' and without the padding zeros, to generate ISR report." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4400 #: model:account.account.template,name:l10n_ch.ch_coa_4400 msgid "Prestations / travaux de tiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3700 #: model:account.account.template,name:l10n_ch.ch_coa_3700 msgid "Prestations propres" msgstr "" #. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.isr_invoice_form -msgid "Print ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_res_company_l10n_ch_isr_print_bank_location -msgid "Print bank location" -msgstr "" - -#. module: l10n_ch -#: model:ir.model.fields,field_description:l10n_ch.field_account_config_settings_l10n_ch_isr_print_bank_location -msgid "Print bank on ISR" -msgstr "" - -#. module: l10n_ch -#: model:ir.ui.view,arch_db:l10n_ch.l10n_ch-isr_account_config_settings -msgid "Print the coordinates of your bank under the 'Payment for' title of the ISR.\n" -" Your address will be moved to the 'in favour of' section." -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7000 #: model:account.account.template,name:l10n_ch.ch_coa_7000 msgid "Produits accessoires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_7500 #: model:account.account.template,name:l10n_ch.ch_coa_7500 msgid "Produits des immeubles d‘exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2301 #: model:account.account.template,name:l10n_ch.ch_coa_2301 msgid "Produits encaissés d’avance" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8510 #: model:account.account.template,name:l10n_ch.ch_coa_8510 msgid "Produits extraordinaires, exceptionnels ou hors période" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6950 #: model:account.account.template,name:l10n_ch.ch_coa_6950 msgid "Produits financiers" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_8100 #: model:account.account.template,name:l10n_ch.ch_coa_8100 msgid "Produits hors exploitation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1301 #: model:account.account.template,name:l10n_ch.ch_coa_1301 msgid "Produits à recevoir" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2980 #: model:account.account.template,name:l10n_ch.ch_coa_2980 msgid "Propres actions, parts sociales, droits de participations (poste négatif)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2600 #: model:account.account.template,name:l10n_ch.ch_coa_2600 msgid "Provisions" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2330 #: model:account.account.template,name:l10n_ch.ch_coa_2330 msgid "Provisions à court terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1440 #: model:account.account.template,name:l10n_ch.ch_coa_1440 msgid "Prêts" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_6600 #: model:account.account.template,name:l10n_ch.ch_coa_6600 msgid "Publicité" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3801 #: model:account.account.template,name:l10n_ch.ch_coa_3801 msgid "Rabais et réduction de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4901 #: model:account.account.template,name:l10n_ch.ch_coa_4901 msgid "Rabais et réductions de prix" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3802 -#: model:account.account,name:l10n_ch.1_ch_coa_4092 #: model:account.account.template,name:l10n_ch.ch_coa_3802 #: model:account.account.template,name:l10n_ch.ch_coa_4092 msgid "Ristournes" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2940 #: model:account.account.template,name:l10n_ch.ch_coa_2940 msgid "Réserves d‘évaluation" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2960 #: model:account.account.template,name:l10n_ch.ch_coa_2960 msgid "Réserves libres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2950 #: model:account.account.template,name:l10n_ch.ch_coa_2950 msgid "Réserves légales issues du bénéfice" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2900 #: model:account.account.template,name:l10n_ch.ch_coa_2900 msgid "Réserves légales issues du capital" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_5000 #: model:account.account.template,name:l10n_ch.ch_coa_5000 msgid "Salaires" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1260 #: model:account.account.template,name:l10n_ch.ch_coa_1260 msgid "Stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1270 #: model:account.account.template,name:l10n_ch.ch_coa_1270 msgid "Stocks de produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:ir.ui.menu,name:l10n_ch.account_reports_ch_statements_menu -msgid "Swiss Statements" -msgstr "" - -#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_dedouanement msgid "Switzerland VAT Form: 100% dédouanement (base in grid 400)" msgstr "" @@ -1208,13 +896,13 @@ msgid "Switzerland VAT Form: grid 280" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_a -msgid "Switzerland VAT Form: grid 301 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_a +msgid "Switzerland VAT Form: grid 302 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_301_b -msgid "Switzerland VAT Form: grid 301 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_302_b +msgid "Switzerland VAT Form: grid 302 tax" msgstr "" #. module: l10n_ch @@ -1228,13 +916,13 @@ msgid "Switzerland VAT Form: grid 311 tax" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_a -msgid "Switzerland VAT Form: grid 341 base" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_a +msgid "Switzerland VAT Form: grid 342 base" msgstr "" #. module: l10n_ch -#: model:account.account.tag,name:l10n_ch.vat_tag_341_b -msgid "Switzerland VAT Form: grid 341 tax" +#: model:account.account.tag,name:l10n_ch.vat_tag_342_b +msgid "Switzerland VAT Form: grid 342 tax" msgstr "" #. module: l10n_ch @@ -1248,6 +936,16 @@ msgid "Switzerland VAT Form: grid 381 tax" msgstr "" #. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_a +msgid "Switzerland VAT Form: grid 382 base" +msgstr "" + +#. module: l10n_ch +#: model:account.account.tag,name:l10n_ch.vat_tag_382_b +msgid "Switzerland VAT Form: grid 382 tax" +msgstr "" + +#. module: l10n_ch #: model:account.account.tag,name:l10n_ch.vat_tag_400 msgid "Switzerland VAT Form: grid 400" msgstr "" @@ -1273,297 +971,228 @@ msgid "Switzerland VAT Form: grid 420" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_import +#: model:account.tax.group,name:l10n_ch.tax_group_tva_0 +msgid "TVA 0%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_O_import msgid "TVA 0% Importations de biens et services" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_O_exclude #: model:account.tax.template,name:l10n_ch.vat_O_exclude msgid "TVA 0% exclue" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase_incl +#: model:account.tax.group,name:l10n_ch.tax_group_tva_25 +msgid "TVA 2.5%" +msgstr "" + +#. module: l10n_ch #: model:account.tax.template,name:l10n_ch.vat_25_purchase_incl msgid "TVA 2.5% sur achat B&S (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_purchase #: model:account.tax.template,name:l10n_ch.vat_25_purchase msgid "TVA 2.5% sur achat B&S (TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest_incl #: model:account.tax.template,name:l10n_ch.vat_25_invest_incl msgid "TVA 2.5% sur invest. et autres ch. (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_invest #: model:account.tax.template,name:l10n_ch.vat_25_invest msgid "TVA 2.5% sur invest. et autres ch. (TR)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_38_purchase_incl -msgid "TVA 3.8% sur achat B&S (Incl. TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_purchase -#: model:account.tax.template,name:l10n_ch.vat_38_purchase -msgid "TVA 3.8% sur achat B&S (TS)" -msgstr "" - -#. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_38_invest_incl -msgid "TVA 3.8% sur invest. et autres ch. (Incl. TS)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_37 +msgid "TVA 3.7%" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_invest -#: model:account.tax.template,name:l10n_ch.vat_38_invest -msgid "TVA 3.8% sur invest. et autres ch. (TS)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase_incl +msgid "TVA 3.7% sur achat B&S (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase_incl -#: model:account.tax.template,name:l10n_ch.vat_80_purchase_incl -msgid "TVA 8.0% sur achat B&S (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_purchase +msgid "TVA 3.7% sur achat B&S (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_purchase -#: model:account.tax.template,name:l10n_ch.vat_80_purchase -msgid "TVA 8.0% sur achat B&S (TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest_incl +msgid "TVA 3.7% sur invest. et autres ch. (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest_incl -#: model:account.tax.template,name:l10n_ch.vat_80_invest_incl -msgid "TVA 8.0% sur invest. et autres ch. (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_37_invest +msgid "TVA 3.7% sur invest. et autres ch. (TS)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_invest -#: model:account.tax.template,name:l10n_ch.vat_80_invest -msgid "TVA 8.0% sur invest. et autres ch. (TN)" +#: model:account.tax.group,name:l10n_ch.tax_group_tva_77 +msgid "TVA 7.7%" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_2200 -#: model:account.account.template,name:l10n_ch.ch_coa_2200 -msgid "TVA due" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_incl +msgid "TVA 7.7% sur achat B&S (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_XO -#: model:account.tax.template,name:l10n_ch.vat_XO -msgid "TVA due a 0% (Exportations)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase +msgid "TVA 7.7% sur achat B&S (TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25 -#: model:account.tax.template,name:l10n_ch.vat_25 -msgid "TVA due a 2.5% (TR)" +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_reverse +msgid "TVA 7.7% sur achat service a l'etranger (reverse charge)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38 -#: model:account.tax.template,name:l10n_ch.vat_38 -msgid "TVA due a 3.8% (TS)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest_incl +msgid "TVA 7.7% sur invest. et autres ch. (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80 -#: model:account.tax.template,name:l10n_ch.vat_80 -msgid "TVA due a 8.0% (TN)" +#: model:account.tax.template,name:l10n_ch.vat_77_invest +msgid "TVA 7.7% sur invest. et autres ch. (TN)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_25_incl -#: model:account.tax.template,name:l10n_ch.vat_25_incl -msgid "TVA due à 2.5% (Incl. TR)" +#: model:account.account.template,name:l10n_ch.ch_coa_2200 +msgid "TVA due" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_38_incl -#: model:account.tax.template,name:l10n_ch.vat_38_incl -msgid "TVA due à 3.8% (Incl. TS)" +#: model:account.tax.template,name:l10n_ch.vat_XO +msgid "TVA due a 0% (Exportations)" msgstr "" #. module: l10n_ch -#: model:account.tax,name:l10n_ch.1_vat_80_incl -#: model:account.tax.template,name:l10n_ch.vat_80_incl -msgid "TVA due à 8.0% (Incl. TN)" +#: model:account.tax.template,name:l10n_ch.vat_25 +msgid "TVA due a 2.5% (TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_partner_bank_l10n_ch_postal -msgid "The ISR number of the company within the bank" +#: model:account.tax.template,name:l10n_ch.vat_37 +msgid "TVA due a 3.7% (TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_currency_name -msgid "The name of this invoice's currency" +#: model:account.tax.template,name:l10n_ch.vat_77 +msgid "TVA due a 7.7% (TN)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_postal -msgid "The postal reference identifying the bank managing this ISR." +#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return +msgid "TVA due a 7.7% (TN) (return)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_chf -msgid "The postal reference of the bank, used to generate ISR payment slips in CHF." +#: model:account.tax.template,name:l10n_ch.vat_25_incl +msgid "TVA due à 2.5% (Incl. TR)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_res_bank_l10n_ch_postal_eur -msgid "The postal reference of the bank, used to generate ISR payment slips in EUR." +#: model:account.tax.template,name:l10n_ch.vat_37_incl +msgid "TVA due à 3.7% (Incl. TS)" msgstr "" #. module: l10n_ch -#: model:ir.model.fields,help:l10n_ch.field_account_invoice_l10n_ch_isr_number -msgid "The reference number associated with this invoice" +#: model:account.tax.template,name:l10n_ch.vat_77_incl +msgid "TVA due à 7.7% (Incl. TN)" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1060 #: model:account.account.template,name:l10n_ch.ch_coa_1060 msgid "Titres" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1400 #: model:account.account.template,name:l10n_ch.ch_coa_1400 msgid "Titres à long terme" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_transfer_account_id -#: model:account.account.template,name:l10n_ch.transfer_account_id -msgid "Transferts internes" -msgstr "" - -#. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1280 #: model:account.account.template,name:l10n_ch.ch_coa_1280 msgid "Travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3940 #: model:account.account.template,name:l10n_ch.ch_coa_3940 msgid "Variation de la valeur des prestations non facturées" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1287 #: model:account.account.template,name:l10n_ch.ch_coa_1287 msgid "Variation de la valeur des travaux en cours" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1277 #: model:account.account.template,name:l10n_ch.ch_coa_1277 msgid "Variation de stock produits semi-ouvrés" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1267 #: model:account.account.template,name:l10n_ch.ch_coa_1267 msgid "Variation de stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1207 -#: model:account.account,name:l10n_ch.1_ch_coa_4800 #: model:account.account.template,name:l10n_ch.ch_coa_1207 #: model:account.account.template,name:l10n_ch.ch_coa_4800 msgid "Variation des stocks de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4801 #: model:account.account.template,name:l10n_ch.ch_coa_4801 msgid "Variation des stocks de matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3901 #: model:account.account.template,name:l10n_ch.ch_coa_3901 msgid "Variation des stocks de produits finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3900 #: model:account.account.template,name:l10n_ch.ch_coa_3900 msgid "Variation des stocks de produits semi-finis" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1217 #: model:account.account.template,name:l10n_ch.ch_coa_1217 msgid "Variation des stocks des matières premières" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_4008 -#: model:account.account,name:l10n_ch.1_ch_coa_4080 #: model:account.account.template,name:l10n_ch.ch_coa_4008 #: model:account.account.template,name:l10n_ch.ch_coa_4080 msgid "Variations de stocks" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3200 #: model:account.account.template,name:l10n_ch.ch_coa_3200 msgid "Ventes de marchandises" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3400 #: model:account.account.template,name:l10n_ch.ch_coa_3400 msgid "Ventes de prestations" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_3000 #: model:account.account.template,name:l10n_ch.ch_coa_3000 msgid "Ventes de produits fabriqués" msgstr "" #. module: l10n_ch -#: model:account.account,name:l10n_ch.1_ch_coa_1530 #: model:account.account.template,name:l10n_ch.ch_coa_1530 msgid "Véhicules" msgstr "" - -#. module: l10n_ch -#: code:addons/l10n_ch/models/account_invoice.py:153 -#, python-format -msgid "You cannot generate an ISR yet.\n" -"\n" -" For this, you need to :\n" -"\n" -" - set a valid postal account number (or an IBAN referencing one) for your company\n" -"\n" -" - define its bank\n" -"\n" -" - associate this bank with a postal reference for the currency used in this invoice\n" -"\n" -" - fill the 'bank account' field of the invoice with the postal to be used to receive the related payment. A default account will be automatically set for all invoices created after you defined a postal account for your company." -msgstr "" - -#. module: l10n_ch -#: model:ir.model,name:l10n_ch.model_account_config_settings -msgid "account.config.settings" -msgstr "" - diff --git a/addons/l10n_it/__manifest__.py b/addons/l10n_it/__manifest__.py index f78432b06b3..fd621d872fd 100644 --- a/addons/l10n_it/__manifest__.py +++ b/addons/l10n_it/__manifest__.py @@ -17,7 +17,7 @@ Piano dei conti italiano di un'impresa generica. Italian accounting chart and localization. """, 'category': 'Localization', - 'website': 'http://www.openerp-italia.org/', + 'website': 'http://www.odoo.com/', 'data': [ 'data/l10n_it_chart_data.xml', 'data/account.account.template.csv', diff --git a/addons/l10n_it_edi/__init__.py b/addons/l10n_it_edi/__init__.py new file mode 100644 index 00000000000..90b17558ffc --- /dev/null +++ b/addons/l10n_it_edi/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import tests diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py new file mode 100644 index 00000000000..fc24a276e68 --- /dev/null +++ b/addons/l10n_it_edi/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Italy - E-invoicing', + 'version': '0.3', + 'depends': ['l10n_it'], + 'author': 'Odoo', + 'description': """ +E-invoice implementation + """, + 'category': 'Localization', + 'website': 'http://www.odoo.com/', + 'data': [ + 'security/ir.model.access.csv', + 'data/invoice_it_template.xml', + 'views/l10n_it_view.xml', + ], + 'demo': [ + 'data/account_invoice_demo.xml', + ], +} diff --git a/addons/l10n_it_edi/data/account_invoice_demo.xml b/addons/l10n_it_edi/data/account_invoice_demo.xml new file mode 100644 index 00000000000..c68a3871df5 --- /dev/null +++ b/addons/l10n_it_edi/data/account_invoice_demo.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <!-- add VAT, codice fiscal and tax system for main company --> + <record id="base.main_company" model="res.company"> + <field name="vat">IT00410123456</field> + <field name="l10n_it_codice_fiscale">0123456789987654</field> + <field name="l10n_it_tax_system">RF01</field> + <field name="zip">12345</field> + </record> + + <record id="base.res_partner_1" model="res.partner"> + <field name="vat">IT11111111123</field> + </record> + <record id="base.res_partner_2" model="res.partner"> + <field name="vat">IT11111111234</field> + </record> + <record id="base.res_partner_12" model="res.partner"> + <field name="vat">IT11111112345</field> + </record> + + </data> +</odoo> diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml new file mode 100644 index 00000000000..c3af915c58c --- /dev/null +++ b/addons/l10n_it_edi/data/invoice_it_template.xml @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <template id="account_invoice_line_it_FatturaPA"> + <t t-set="taxes" t-value="line.invoice_line_tax_ids.compute_all(line.price_unit)"/> + <DettaglioLinee> + <NumeroLinea t-esc="line_counter"/> + <CodiceArticolo t-if="line.product_id.barcode"> + <!--2.2.1.3--> + <CodiceTipo>EAN</CodiceTipo> + <CodiceValore t-esc="line.product_id.barcode"/> + </CodiceArticolo> + <CodiceArticolo t-if="line.product_id.default_code"> + <CodiceTipo>INTERNAL</CodiceTipo> + <CodiceValore t-esc="line.product_id.default_code"/> + </CodiceArticolo> + <Descrizione> + <t t-esc="line.name[:1000]"/> + <t t-if="not line.name" t-esc="'NO NAME'"/> + </Descrizione> + <Quantita t-esc="format_numbers(line.quantity)"/> + <UnitaMisura t-if="line.uom_id.category_id.measure_type != 'unit'" t-esc="line.uom_id.name"/> + <PrezzoUnitario t-esc="format_monetary(taxes['total_excluded'], currency)"/> + <ScontoMaggiorazione t-if="line.discount != 0"> + <!-- [2.2.1.10] --> + <Tipo t-esc="discount_type(line.discount)"/> + <Percentuale t-esc="format_numbers(abs(line.discount))"/> + </ScontoMaggiorazione> + <PrezzoTotale t-esc="format_monetary(line.price_subtotal, currency)"/> + <!-- without tax, must include any discounts and any extra charge--> + <AliquotaIVA t-if="line.invoice_line_tax_ids.amount_type == 'percent'" t-esc="format_numbers(line.invoice_line_tax_ids.amount)"/> + <AliquotaIVA t-if="line.invoice_line_tax_ids.amount_type != 'percent'" t-esc="'0.00'"/> + <Natura t-if="line.invoice_line_tax_ids.l10n_it_has_exoneration" t-esc="line.invoice_line_tax_ids.l10n_it_kind_exoneration"/> + </DettaglioLinee> + </template> + + <template id="account_invoice_it_FatturaPA_sede"> + <Sede> + <Indirizzo><t t-if="partner.street" t-esc="partner.street"/> <t t-if="partner.street2" t-esc="partner.street2"/></Indirizzo> + <CAP><t t-if="partner.country_id.code != 'IT'" t-esc="'00000'"/><t t-else="" t-esc="partner.zip"/></CAP> + <Comune t-esc="partner.city"/> + <Provincia t-if="partner.country_id.code == 'IT'" t-esc="partner.state_id.code"/> + <Nazione t-esc="partner.country_id.code"/> + </Sede> + </template> + + <template id="account_invoice_it_FatturaPA_export"> + <t t-set="currency" t-value="record.currency_id or record.company_currency_id"/> + <t t-set="bank" t-value="record.partner_bank_id"/> + <p:FatturaElettronica t-att-versione="formato_trasmissione" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd"> + <FatturaElettronicaHeader> + <DatiTrasmissione> + <IdTrasmittente> + <IdPaese t-esc="get_vat_country(record.company_id.vat)"/> + <IdCodice t-esc="record.company_id.l10n_it_codice_fiscale"/> + </IdTrasmittente> + <ProgressivoInvio t-esc="record.number.replace('/','')[-10:]"/> + <FormatoTrasmissione t-esc="formato_trasmissione"/> + <CodiceDestinatario t-if="record.commercial_partner_id.l10n_it_pa_index and record.commercial_partner_id.country_id.code == 'IT'" t-esc="record.commercial_partner_id.l10n_it_pa_index.upper()"/> + <CodiceDestinatario t-if="not record.commercial_partner_id.l10n_it_pa_index and record.commercial_partner_id.country_id.code == 'IT'" t-esc="'0000000'"/> + <CodiceDestinatario t-if="record.commercial_partner_id.country_id.code != 'IT'" t-esc="'XXXXXXX'"/> + <ContattiTrasmittente> + <Telefono t-if="format_phone(record.company_id.partner_id.phone)" t-esc="format_phone(record.company_id.partner_id.phone)"/> + <Telefono t-if="not format_phone(record.company_id.partner_id.phone) and format_phone(record.company_id.partner_id.mobile)" t-esc="format_phone(record.company_id.partner_id.mobile)"/> + <Email t-if="record.company_id.email" t-esc="record.company_id.email"/> + </ContattiTrasmittente> + <PECDestinatario t-if="record.commercial_partner_id.l10n_it_pec_email" t-esc="record.commercial_partner_id.l10n_it_pec_email"/> + </DatiTrasmissione> + <CedentePrestatore> + <DatiAnagrafici> + <IdFiscaleIVA> + <IdPaese t-esc="get_vat_country(record.company_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.company_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-esc="record.company_id.partner_id.display_name"/> + </Anagrafica> + <RegimeFiscale t-esc="record.company_id.l10n_it_tax_system"/> + </DatiAnagrafici> + <t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede"> + <t t-set="partner" t-value="record.company_id.partner_id"/> + </t> + <IscrizioneREA t-if="record.company_id.l10n_it_has_eco_index"> + <!--1.2.4--> + <Ufficio t-esc="record.company_id.l10n_it_eco_index_office.code"/> + <NumeroREA t-esc="record.company_id.l10n_it_eco_index_number"/> + <CapitaleSociale t-if="record.company_id.l10n_it_eco_index_share_capital != 0" t-esc="format_numbers_two(record.company_id.l10n_it_eco_index_share_capital)"/> + <SocioUnico t-if="record.company_id.l10n_it_eco_index_sole_shareholder != 'NO'" t-esc="record.company_id.l10n_it_eco_index_sole_shareholder"/> + <StatoLiquidazione t-esc="record.company_id.l10n_it_eco_index_liquidation_state"/> + </IscrizioneREA> + </CedentePrestatore> + <RappresentanteFiscale t-if="record.company_id.l10n_it_has_tax_representative"> + <!--1.3--> + <DatiAnagrafici> + <IdFiscaleIVA> + <IdPaese t-esc="get_vat_country(record.company_id.l10n_it_tax_representative_partner_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.company_id.l10n_it_tax_representative_partner_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="record.company_id.l10n_it_tax_representative_partner_id.l10n_it_codice_fiscale" t-esc="record.company_id.l10n_it_tax_representative_partner_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-if="record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.display_name"/> + <Nome t-if="not record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.name"/> + <Cognome t-if="not record.company_id.l10n_it_tax_representative_partner_id.is_company" t-esc="record.company_id.l10n_it_tax_representative_partner_id.name"/> + </Anagrafica> + </DatiAnagrafici> + </RappresentanteFiscale> + <CessionarioCommittente> + <DatiAnagrafici> + <IdFiscaleIVA t-if="record.commercial_partner_id.vat"> + <IdPaese t-esc="get_vat_country(record.commercial_partner_id.vat)"/> + <IdCodice t-esc="get_vat_number(record.commercial_partner_id.vat)"/> + </IdFiscaleIVA> + <CodiceFiscale t-if="not record.commercial_partner_id.vat" t-esc="record.commercial_partner_id.l10n_it_codice_fiscale"/> + <Anagrafica> + <Denominazione t-if="record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.display_name"/> + <Nome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/> + <Cognome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/> + </Anagrafica> + </DatiAnagrafici> + <t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede"> + <t t-set="partner" t-value="record.commercial_partner_id"/> + </t> + </CessionarioCommittente> + </FatturaElettronicaHeader> + <FatturaElettronicaBody> + <DatiGenerali> + <DatiGeneraliDocumento> + <!--2.1.1--> + <TipoDocumento t-esc="document_type"/> + <Divisa t-esc="currency.name"/> + <Data t-esc="format_date(record.date_invoice)"/> + <Numero t-esc="record.number[-20:]"/> + <DatiBollo t-if="record.l10n_it_stamp_duty"> + <!--2.1.1.6--> + <BolloVirtuale>SI</BolloVirtuale> + <ImportoBollo t-esc="format_numbers(record.l10n_it_stamp_duty)"/> + </DatiBollo> + </DatiGeneraliDocumento> + <DatiDDT t-if="record.l10n_it_ddt_id"> + <!--2.1.8--> + <NumeroDDT t-esc="record.l10n_it_ddt_id.name"/> + <DataDDT t-esc="format_date(record.l10n_it_ddt_id.date)"/> + </DatiDDT> + </DatiGenerali> + <DatiBeniServizi> + <!-- Invoice lines. --> + <t t-set="line_counter" t-value="0"/> + <t t-foreach="record.invoice_line_ids.filtered(lambda l: not l.display_type)" t-as="line"> + <t t-set="line_counter" t-value="line_counter + 1"/> + <t t-call="l10n_it_edi.account_invoice_line_it_FatturaPA"/> + </t> + <t t-foreach="record.tax_line_ids" t-as="tax_line"> + <DatiRiepilogo> + <!--2.2.2--> + <AliquotaIVA t-esc="format_numbers(tax_line.tax_id.amount)"/> + <Natura t-if="tax_line.tax_id.l10n_it_has_exoneration" t-esc="tax_line.tax_id.l10n_it_kind_exoneration"/> + <ImponibileImporto t-esc="format_monetary(tax_line.base, currency)"/> + <Imposta t-esc="format_monetary(tax_line.amount, currency)"/> + <EsigibilitaIVA t-if="not tax_line.tax_id.l10n_it_has_exoneration or tax_line.tax_id.l10n_it_kind_exoneration=='N6'" t-esc="tax_line.tax_id.l10n_it_vat_due_date"/> + <RiferimentoNormativo t-if="tax_line.tax_id.l10n_it_has_exoneration" t-esc="tax_line.tax_id.l10n_it_law_reference"/> + </DatiRiepilogo> + </t> + + </DatiBeniServizi> + <Allegati t-if="pdf"> + <NomeAttachment t-esc="pdf_name"/> + <FormatoAttachment>PDF</FormatoAttachment> + <Attachment t-esc="pdf"/> + </Allegati> + </FatturaElettronicaBody> + </p:FatturaElettronica> + </template> + </data> +</odoo> diff --git a/addons/l10n_it_edi/models/__init__.py b/addons/l10n_it_edi/models/__init__.py new file mode 100644 index 00000000000..a7b2d212437 --- /dev/null +++ b/addons/l10n_it_edi/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import res_partner +from . import res_company +from . import account_invoice +from . import ir_mail_server +from . import ddt diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py new file mode 100644 index 00000000000..123081e5503 --- /dev/null +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -0,0 +1,752 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import zipfile +import io +import logging +import re + +from datetime import date, datetime +from lxml import etree + +from odoo import api, fields, models, _ +from odoo.tools import float_repr +from odoo.exceptions import UserError, ValidationError +from odoo.addons.base.models.ir_mail_server import MailDeliveryException +from odoo.tests.common import Form + + +_logger = logging.getLogger(__name__) + +DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d' + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + _name = 'account.invoice' + + l10n_it_send_state = fields.Selection([ + ('new', 'New'), + ('other', 'Other'), + ('to_send', 'Not yet send'), + ('sent', 'Sent, waiting for response'), + ('invalid', 'Sent, but invalid'), + ('delivered', 'This invoice is delivered'), + ('delivered_accepted', 'This invoice is delivered and accepted by destinatory'), + ('delivered_refused', 'This invoice is delivered and refused by destinatory'), + ('delivered_expired', 'This invoice is delivered and expired (expiry of the maximum term for communication of acceptance/refusal)'), + ('failed_delivery', 'Delivery impossible, ES certify that it has received the invoice and that the file \ + could not be delivered to the addressee') # ok we must do nothing + ], default='to_send', copy=False) + + l10n_it_stamp_duty = fields.Float(default=0, string="Dati Bollo", size=15, readonly=True, states={'draft': [('readonly', False)]}) + + l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False) + + l10n_it_einvoice_name = fields.Char(readonly=True, copy=False) + + l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", copy=False) + + @api.multi + def invoice_validate(self): + super(AccountInvoice, self).invoice_validate() + for invoice in self: + if invoice.company_id.country_id != self.env.ref('base.it'): + continue + if invoice.type == 'in_invoice' or invoice.type == 'in_refund': + invoice.l10n_it_send_state = "other" + continue + + invoice._check_before_xml_exporting() + + invoice.invoice_generate_xml() + if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6: + invoice.message_post( + body=(_("Invoices for PA are not managed by Odoo, you can download the document and send it on your own.")) + ) + invoice.l10n_it_send_state = "other" + continue + invoice.l10n_it_send_state = "to_send" + invoice.send_pec_mail() + + def _check_before_xml_exporting(self): + seller = self.company_id + buyer = self.commercial_partner_id + + # <1.1.1.1> + if not seller.country_id: + raise UserError(_("%s must have a country") % (seller.display_name)) + + # <1.1.1.2> + if not seller.vat: + raise UserError(_("%s must have a VAT number") % (seller.display_name)) + elif len(seller.vat) > 30: + raise UserError(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.") % (seller.display_name, seller.vat)) + + # <1.2.1.2> + if not seller.l10n_it_codice_fiscale: + raise UserError(_("%s must have a codice fiscale number") % (seller.display_name)) + + # <1.2.1.8> + if not seller.l10n_it_tax_system: + raise UserError("The seller's company must have a tax system.") + + # <1.2.2> + if not seller.street and not seller.street2: + raise UserError(_("%s must have a street.") % (seller.display_name)) + if not seller.zip: + raise UserError(_("%s must have a post code.") % (seller.display_name)) + if len(seller.zip) != 5 and seller.country_id.code == 'IT': + raise UserError(_("%s must have a post code of length 5.") % (seller.display_name)) + if not seller.city: + raise UserError(_("%s must have a city.") % (seller.display_name)) + if not seller.country_id: + raise UserError(_("%s must have a country.") % (seller.display_name)) + + # <1.4.1> + if not buyer.vat and not buyer.l10n_it_codice_fiscale: + raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name)) + + # <1.4.2> + if not buyer.street and not buyer.street2: + raise UserError(_("%s must have a street.") % (buyer.display_name)) + if not buyer.zip: + raise UserError(_("%s must have a post code.") % (buyer.display_name)) + if len(buyer.zip) != 5 and buyer.country_id.code == 'IT': + raise UserError(_("%s must have a post code of length 5.") % (buyer.display_name)) + if not buyer.city: + raise UserError(_("%s must have a city.") % (buyer.display_name)) + if not buyer.country_id: + raise UserError(_("%s must have a country.") % (buyer.display_name)) + + # <2.2.1> + for invoice_line in self.invoice_line_ids: + if len(invoice_line.invoice_line_tax_ids) != 1: + raise UserError(_("You must select one and only one tax by line.")) + + for tax_line in self.tax_line_ids: + if not tax_line.tax_id.l10n_it_has_exoneration and tax_line.tax_id.amount == 0: + raise ValidationError(_("%s has an amount of 0.0, you must indicate the kind of exoneration." % tax_line.name)) + + @api.multi + def invoice_generate_xml(self): + for invoice in self: + if invoice.l10n_it_einvoice_id and invoice.l10n_it_send_state not in ['invalid', 'to_send']: + raise UserError(_("You can't regenerate an E-Invoice when the first one is sent and there are no errors")) + if invoice.l10n_it_einvoice_id: + invoice.l10n_it_einvoice_id.unlink() + + a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + n = invoice.id + progressive_number = "" + while n: + (n,m) = divmod(n,len(a)) + progressive_number = a[m] + progressive_number + + report_name = '%(country_code)s%(codice)s_%(progressive_number)s.xml' % { + 'country_code': invoice.company_id.country_id.code, + 'codice': invoice.company_id.l10n_it_codice_fiscale, + 'progressive_number': progressive_number.zfill(5), + } + invoice.l10n_it_einvoice_name = report_name + + data = b"<?xml version='1.0' encoding='UTF-8'?>" + invoice._export_as_xml() + description = _('Italian invoice: %s') % invoice.type + invoice.l10n_it_einvoice_id = self.env['ir.attachment'].create({ + 'name': report_name, + 'res_id': invoice.id, + 'res_model': invoice._name, + 'datas': base64.encodestring(data), + 'datas_fname': report_name, + 'description': description, + 'type': 'binary', + }) + + invoice.message_post( + body=(_("E-Invoice is generated on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + + def _export_as_xml(self): + ''' Create the xml file content. + :return: The XML content as str. + ''' + self.ensure_one() + + def format_date(dt): + # Format the date in the italian standard. + dt = dt or datetime.now() + return dt.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + def format_monetary(number, currency): + # Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001). + return float_repr(number, min(2, currency.decimal_places)) + + def format_numbers(number): + #format number to str with between 2 and 8 decimals (event if it's .00) + number_splited = str(number).split('.') + if len(number_splited) == 1: + return "%.02f" % number + + cents = number_splited[1] + if len(cents) > 8: + return "%.08f" % number + return float_repr(number, max(2, len(cents))) + + def format_numbers_two(number): + #format number to str with 2 (event if it's .00) + return "%.02f" % number + + def discount_type(discount): + return 'SC' if discount > 0 else 'MG' + + def format_phone(number): + if not number: + return False + number = number.replace(' ', '').replace('/', '').replace('.', '') + if len(number) > 4 and len(number) < 13: + return number + return False + + def get_vat_number(vat): + return vat[2:].replace(' ', '') + + def get_vat_country(vat): + return vat[:2].upper() + + formato_trasmissione = "FPR12" + if len(self.commercial_partner_id.l10n_it_pa_index or '1') == 6: + formato_trasmissione = "FPA12" + + if self.type == 'out_invoice': + document_type = 'TD01' + elif self.type == 'out_refund': + document_type = 'TD04' + else: + document_type = 'TD0X' + + pdf = self.env.ref('account.account_invoices').render_qweb_pdf(self.id)[0] + pdf = base64.b64encode(pdf) + pdf_name = re.sub(r'\W+', '', self.number) + '.pdf' + + # Create file content. + template_values = { + 'record': self, + 'format_date': format_date, + 'format_monetary': format_monetary, + 'format_numbers': format_numbers, + 'format_numbers_two': format_numbers_two, + 'format_phone': format_phone, + 'discount_type': discount_type, + 'get_vat_number': get_vat_number, + 'get_vat_country': get_vat_country, + 'abs': abs, + 'formato_trasmissione': formato_trasmissione, + 'document_type': document_type, + 'pdf': pdf, + 'pdf_name': pdf_name, + } + content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export').render(template_values) + return content + + @api.multi + def send_pec_mail(self): + self.ensure_one() + allowed_state = ['to_send', 'invalid'] + + if not self.company_id.l10n_it_mail_pec_server_id or not self.company_id.l10n_it_address_send_fatturapa: + self.message_post( + body=(_("Error when sending mail with E-Invoice: Your company must have a mail PEC server and must indicate the mail PEC that will send electronic invoice.")) + ) + self.l10n_it_send_state = 'invalid' + return + + if self.l10n_it_send_state not in allowed_state: + raise UserError(_("%s isn't in a right state. It must be in a 'Not yet send' or 'Invalid' state.") % (self.display_name)) + + message = self.env['mail.message'].create({ + 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_id.name), + 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.user.company_id.l10n_it_address_recipient_fatturapa), + 'email_from': self.env.user.company_id.l10n_it_address_send_fatturapa, + 'mail_server_id': self.env.user.company_id.l10n_it_mail_pec_server_id.id, + 'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)], + }) + + mail_fattura = self.env['mail.mail'].create({ + 'mail_message_id': message.id, + 'email_to': self.env.user.company_id.l10n_it_address_recipient_fatturapa, + }) + try: + mail_fattura.send(raise_exception=True) + self.message_post( + body=(_("Mail sent on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + self.l10n_it_send_state = 'sent' + except MailDeliveryException as error: + self.message_post( + body=(_("Error when sending mail with E-Invoice: %s") % (error.args[0])) + ) + self.l10n_it_send_state = 'invalid' + + def _import_xml_invoice(self, content, attachment): + ''' Extract invoice values from the E-Invoice xml tree passed as parameter. + + :param content: The tree of the xml file. + :return: A dictionary containing account.invoice values to create/update it. + ''' + + try: + tree = etree.fromstring(content) + except: + _logger.info('Error during decoding XML file') + return self.env['account.invoice'] + + invoices = self.env['account.invoice'] + + # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch + for body_tree in tree.xpath('//FatturaElettronicaBody', namespaces=tree.nsmap): + + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento', namespaces=tree.nsmap) + if elements and elements[0].text and elements[0].text == 'TD01': + self_ctx = self.with_context(type='in_invoice') + elif elements and elements[0].text and elements[0].text == 'TD04': + self_ctx = self.with_context(type='in_refund') + else: + _logger.info(_('Document type not managed: %s.') % (elements[0].text)) + + # type must be present in the context to get the right behavior of the _default_journal method (account.invoice). + # journal_id must be present in the context to get the right behavior of the _default_account method (account.invoice.line). + + elements = tree.xpath('//CessionarioCommittente//IdCodice', namespaces=tree.nsmap) + company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1) + if not company: + elements = tree.xpath('//CessionarioCommittente//CodiceFiscale', namespaces=tree.nsmap) + company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1) + + if company: + self_ctx = self_ctx.with_context(company_id=company.id) + else: + company = self.env.user.company_id + if elements: + _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text) + else: + _logger.info(_('Company not found. The company\'s user is set by default.')) + + if not self.env.user._is_superuser(): + if self.env.user.company_id != company: + raise UserError(_("You can only import invoice concern your current company: %s") % self.env.user.company_id.display_name) + + journal_id = self_ctx._default_journal().id + self_ctx = self_ctx.with_context(journal_id=journal_id) + + # self could be a single record (editing) or be empty (new). + with Form(self_ctx, view='account.invoice_supplier_form') as invoice_form: + message_to_log = [] + + invoice_form.company_id = company + + # Refund type. + # TD01 == invoice + # TD02 == advance/down payment on invoice + # TD03 == advance/down payment on fee + # TD04 == credit note + # TD05 == debit note + # TD06 == fee + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento', namespaces=tree.nsmap) + if elements and elements[0].text and elements[0].text == 'TD01': + invoice_form.type = 'in_invoice' + elif elements and elements[0].text and elements[0].text == 'TD04': + invoice_form.type = 'in_refund' + + # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> + elements = tree.xpath('//CedentePrestatore//IdCodice', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//CedentePrestatore//CodiceFiscale', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//DatiTrasmissione//Email', namespaces=tree.nsmap) + partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if partner: + invoice_form.partner_id = partner + else: + message_to_log.append("%s<br/>%s" % ( + _("Vendor not found, useful informations from XML file:"), + self._compose_info_message( + tree, './/CedentePrestatore'))) + + # Numbering attributed by the transmitter. <1.1.2> + elements = tree.xpath('//ProgressivoInvio', namespaces=tree.nsmap) + if elements: + invoice_form.name = elements[0].text + + elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero', namespaces=body_tree.nsmap) + if elements: + invoice_form.reference = elements[0].text + + # Currency. <2.1.1.2> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa', namespaces=body_tree.nsmap) + if elements: + currency_str = elements[0].text + currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False) + if currency != self.env.user.company_id.currency_id and currency.active: + invoice_form.currency_id = currency + + # Date. <2.1.1.3> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Data', namespaces=body_tree.nsmap) + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.date_invoice = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + # Dati Bollo. <2.1.1.6> + elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo', namespaces=body_tree.nsmap) + if elements: + invoice_form.l10n_it_stamp_duty = float(elements[0].text) + + # List of all amount discount (will be add after all article to avoid to have a negative sum) + discount_list = [] + percentage_global_discount = 1.0 + + # Global discount. <2.1.1.8> + discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione', namespaces=body_tree.nsmap) + total_discount_amount = 0.0 + if discount_elements: + for discount_element in discount_elements: + discount_line = discount_element.xpath('.//Tipo', namespaces=body_tree.nsmap) + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = discount_element.xpath('.//Percentuale', namespaces=body_tree.nsmap) + if discount_percentage and discount_percentage[0].text: + percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign + + discount_amount_text = discount_element.xpath('.//Importo', namespaces=body_tree.nsmap) + if discount_amount_text and discount_amount_text[0].text: + discount_amount = float(discount_amount_text[0].text) * discount_sign * -1 + discount = {} + discount["seq"] = 0 + + if discount_amount < 0: + discount["name"] = _('GLOBAL DISCOUNT') + else: + discount["name"] = _('GLOBAL EXTRA CHARGE') + discount["amount"] = discount_amount + discount["tax"] = [] + discount_list.append(discount) + + # Comment. <2.1.1.11> + elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale', namespaces=body_tree.nsmap) + for element in elements: + invoice_form.comment = '%s%s\n' % (invoice_form.comment or '', element.text) + + # Informations relative to the purchase order, the contract, the agreement, + # the reception phase or invoices previously transmitted + # <2.1.2> - <2.1.6> + for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']: + elements = body_tree.xpath('.//DatiGenerali/' + document_type, namespaces=body_tree.nsmap) + if elements: + for element in elements: + message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"), + self._compose_info_message(element, '.'))) + + # Dati DDT. <2.1.8> + elements = body_tree.xpath('.//DatiGenerali/DatiDDT', namespaces=body_tree.nsmap) + if elements: + message_to_log.append("%s<br/>%s" % ( + _("Transport informations from XML file:"), + self._compose_info_message(body_tree, './/DatiGenerali/DatiDDT'))) + + # Due date. <2.4.2.5> + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento', namespaces=body_tree.nsmap) + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.date_due = fields.Date.to_string(date_obj) + + # Total amount. <2.4.2.6> + elements = body_tree.xpath('.//ImportoPagamento', namespaces=body_tree.nsmap) + amount_total_import = 0 + for element in elements: + amount_total_import += float(element.text) + if amount_total_import: + message_to_log.append(_("Total amount from the XML File: %s") % ( + amount_total_import)) + + # Bank account. <2.4.2.13> + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN', namespaces=body_tree.nsmap) + if elements: + if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id: + bank = self.env['res.partner.bank'].search([ + ('acc_number', '=', elements[0].text), + ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id) + ]) + else: + bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)]) + if bank: + invoice_form.partner_bank_id = bank + else: + message_to_log.append("%s<br/>%s" % ( + _("Bank account not found, useful informations from XML file:"), + self._compose_multi_info_message( + body_tree, ['.//DatiPagamento//Beneficiario', + './/DatiPagamento//IstitutoFinanziario', + './/DatiPagamento//IBAN', + './/DatiPagamento//ABI', + './/DatiPagamento//CAB', + './/DatiPagamento//BIC', + './/DatiPagamento//ModalitaPagamento']))) + else: + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento', namespaces=body_tree.nsmap) + if elements: + message_to_log.append("%s<br/>%s" % ( + _("Bank account not found, useful informations from XML file:"), + self._compose_info_message(body_tree, './/DatiPagamento'))) + + # Invoice lines. <2.2.1> + elements = body_tree.xpath('.//DettaglioLinee', namespaces=body_tree.nsmap) + if elements: + for element in elements: + with invoice_form.invoice_line_ids.new() as invoice_line_form: + + # Sequence. + line_elements = element.xpath('.//NumeroLinea', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.sequence = int(line_elements[0].text) * 2 + + # Product. + line_elements = element.xpath('.//Descrizione', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.name = " ".join(line_elements[0].text.split()) + + elements_code = element.xpath('.//CodiceArticolo', namespaces=body_tree.nsmap) + if elements_code: + for element_code in elements_code: + type_code = element_code.xpath('.//CodiceTipo', namespaces=body_tree.nsmap)[0] + code = element_code.xpath('.//CodiceValore', namespaces=body_tree.nsmap)[0] + if type_code.text == 'EAN': + product = self.env['product.product'].search([('barcode', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + if partner: + product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)]) + if product_supplier and product_supplier.product_id: + invoice_line_form.product_id = product_supplier.product_id + break + if not invoice_line_form.product_id: + for element_code in elements_code: + code = element_code.xpath('.//CodiceValore', namespaces=body_tree.nsmap)[0] + product = self.env['product.product'].search([('default_code', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + + # Price Unit. + line_elements = element.xpath('.//PrezzoUnitario', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.price_unit = float(line_elements[0].text) + + # Quantity. + line_elements = element.xpath('.//Quantita', namespaces=body_tree.nsmap) + if line_elements: + invoice_line_form.quantity = float(line_elements[0].text) + else: + invoice_line_form.quantity = 1 + + # Taxes + tax_element = element.xpath('.//AliquotaIVA', namespaces=body_tree.nsmap) + natura_element = element.xpath('.//Natura', namespaces=body_tree.nsmap) + invoice_line_form.invoice_line_tax_ids.clear() + if tax_element and tax_element[0].text: + percentage = float(tax_element[0].text) + if natura_element and natura_element[0].text: + l10n_it_kind_exoneration = natura_element[0].text + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration), + ], limit=1) + else: + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ], limit=1) + l10n_it_kind_exoneration = '' + + if tax: + invoice_line_form.invoice_line_tax_ids.add(tax) + else: + if l10n_it_kind_exoneration: + message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % ( + percentage, + l10n_it_kind_exoneration, + invoice_line_form.name)) + else: + message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % ( + percentage, + invoice_line_form.name)) + + # Discount in cascade mode. + # if 3 discounts : -10% -50€ -20% + # the result must be : (((price -10%)-50€) -20%) + # Generic form : (((price -P1%)-A1€) -P2%) + # It will be split in two parts: fix amount and pourcent amount + # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€) + # pourcent: 1-(1-P2)*(1-P5) + # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all + # percentage present after the fix amount) + line_elements = element.xpath('.//ScontoMaggiorazione', namespaces=body_tree.nsmap) + total_discount_amount = 0.0 + total_discount_percentage = percentage_global_discount + if line_elements: + for line_element in line_elements: + discount_line = line_element.xpath('.//Tipo', namespaces=body_tree.nsmap) + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = line_element.xpath('.//Percentuale', namespaces=body_tree.nsmap) + if discount_percentage and discount_percentage[0].text: + pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign + total_discount_percentage *= pourcentage_actual + total_discount_amount *= pourcentage_actual + + discount_amount = line_element.xpath('.//Importo', namespaces=body_tree.nsmap) + if discount_amount and discount_amount[0].text: + total_discount_amount += float(discount_amount[0].text) * discount_sign * -1 + + # Save amount discount. + if total_discount_amount != 0: + discount = {} + discount["seq"] = invoice_line_form.sequence + 1 + + if total_discount_amount < 0: + discount["name"] = _('DISCOUNT: ') + invoice_line_form.name + else: + discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name + discount["amount"] = total_discount_amount + discount["tax"] = [] + for tax in invoice_line_form.invoice_line_tax_ids: + discount["tax"].append(tax) + discount_list.append(discount) + invoice_line_form.discount = (1 - total_discount_percentage) * 100 + + # Apply amount discount. + for discount in discount_list: + with invoice_form.invoice_line_ids.new() as invoice_line_form_discount: + invoice_line_form_discount.invoice_line_tax_ids.clear() + invoice_line_form_discount.sequence = discount["seq"] + invoice_line_form_discount.name = discount["name"] + invoice_line_form_discount.price_unit = discount["amount"] + + new_invoice = invoice_form.save() + new_invoice.l10n_it_send_state = "other" + + elements = body_tree.xpath('.//Allegati', namespaces=body_tree.nsmap) + if elements: + for element in elements: + name_attachment = element.xpath('.//NomeAttachment', namespaces=body_tree.nsmap)[0].text + attachment_64 = str.encode(element.xpath('.//Attachment', namespaces=body_tree.nsmap)[0].text) + attachment_64 = self.env['ir.attachment'].create({ + 'name': name_attachment, + 'datas': attachment_64, + 'datas_fname': name_attachment, + 'type': 'binary', + }) + + # default_res_id is had to context to avoid facturx to import his content + new_invoice.with_context(default_res_id=new_invoice.id).message_post( + body=(_("Attachment from XML")), + attachment_ids=[attachment_64.id] + ) + + for message in message_to_log: + new_invoice.message_post(body=message) + + if attachment: + new_invoice.l10n_it_einvoice_name = attachment.name + attachment.write({'res_model': 'account.invoice', 'res_id': new_invoice.id}) + new_invoice.message_post(attachment_ids=[attachment.id]) + invoices += new_invoice + return invoices + + def _compose_info_message(self, tree, element_tags): + output_str = "" + elements = tree.xpath(element_tags, namespaces=tree.nsmap) + for element in elements: + output_str += "<ul>" + for line in element.iter(): + if line.text: + text = " ".join(line.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (line.tag, text) + output_str += "</ul>" + return output_str + + def _compose_multi_info_message(self, tree, element_tags): + output_str = "<ul>" + + for element_tag in element_tags: + elements = tree.xpath(element_tag, namespaces=tree.nsmap) + if not elements: + continue + for element in elements: + text = " ".join(element.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (element.tag, text) + return output_str + "</ul>" + +class AccountTax(models.Model): + _name = "account.tax" + _inherit = "account.tax" + + l10n_it_vat_due_date = fields.Selection([ + ("I", "[I] IVA ad esigibilità immediata"), + ("D", "[D] IVA ad esigibilità differita"), + ("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date") + + l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.") + l10n_it_kind_exoneration = fields.Selection(selection=[ + ("N1", "[N1] Escluse ex art. 15"), + ("N2", "[N2] Non soggette"), + ("N3", "[N3] Non imponibili"), + ("N4", "[N4] Esenti"), + ("N5", "[N5] Regime del margine / IVA non esposta in fattura"), + ("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"), + ("N7", "[N7] IVA assolta in altro stato UE (vendite a distanza ex art. 40 c. 3 e 4 e art. 41 c. 1 lett. b, DL 331/93; prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-sexies lett. f, g, art. 74-sexies DPR 633/72)")], + string="Exoneration", + help="Exoneration type", + default="N1") + l10n_it_law_reference = fields.Char(string="Law Reference", size=100) + + @api.constrains('l10n_it_has_exoneration', + 'l10n_it_kind_exoneration', + 'l10n_it_law_reference', + 'amount', + 'l10n_it_vat_due_date') + def _check_exoneration_with_no_tax(self): + for tax in self: + if tax.l10n_it_has_exoneration: + if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0: + raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.") + if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S': + raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'")) + +class ImportInvoiceImportWizard(models.TransientModel): + _name = 'account.invoice.import.wizard' + _inherit = 'account.invoice.import.wizard' + + @api.multi + def _create_invoice_from_file(self, attachment): + if attachment.mimetype == 'application/xml' and re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.name): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', attachment.name)], limit=1): + # invoice already exist + raise UserError(_('E-invoice already exist: %s') % attachment.name) + self = self.with_context(default_journal_id= self.journal_id.id) + invoice = self.env['account.invoice']._import_xml_invoice(base64.decodestring(attachment.datas), attachment) + else: + invoice = super(ImportInvoiceImportWizard, self)._create_invoice_from_file(attachment) + return invoice diff --git a/addons/l10n_it_edi/models/ddt.py b/addons/l10n_it_edi/models/ddt.py new file mode 100644 index 00000000000..61464a7d76a --- /dev/null +++ b/addons/l10n_it_edi/models/ddt.py @@ -0,0 +1,21 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + +class L10nItDdt(models.Model): + _name = 'l10n_it.ddt' + _description = 'Transport Document' + + invoice_id = fields.One2many('account.invoice', 'l10n_it_ddt_id', string='Invoice Reference', + ondelete='cascade') + + name = fields.Char(string="Numero DDT", size=20, help="Transport document number", required=True) + date = fields.Date(string="Data DDT", help="Transport document date", required=True) + + @api.multi + def name_get(self): + res = [] + for ddt in self: + res.append((ddt.id, ("%s (%s)") % (ddt.name, ddt.date))) + return res diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py new file mode 100644 index 00000000000..3bfce47100d --- /dev/null +++ b/addons/l10n_it_edi/models/ir_mail_server.py @@ -0,0 +1,358 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import zipfile +import io +import re +import logging +import email +import dateutil +import pytz +import base64 +try: + from xmlrpc import client as xmlrpclib +except ImportError: + import xmlrpclib + + +from lxml import etree +from datetime import datetime + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import ValidationError + + +_logger = logging.getLogger(__name__) + +class FetchmailServer(models.Model): + _name = 'fetchmail.server' + _inherit = 'fetchmail.server' + + l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.") + l10n_it_last_uid = fields.Integer(string='Last message UID', default=1) + + @api.constrains('l10n_it_is_pec', 'type') + def _check_pec(self): + for record in self: + if record.l10n_it_is_pec and record.type != 'imap': + raise ValidationError("PEC mail server must be of type IMAP.") + + @api.multi + def fetch_mail(self): + """ WARNING: meant for cron usage only - will commit() after each email! """ + + MailThread = self.env['mail.thread'] + for server in self.filtered(lambda s: s.l10n_it_is_pec): + _logger.info('start checking for new emails on %s PEC server %s', server.type, server.name) + + count, failed = 0, 0 + imap_server = None + try: + imap_server = server.connect() + imap_server.select() + + result, data = imap_server.uid('search', None, '(FROM "@pec.fatturapa.it")', '(UID %s:*)' % (server.l10n_it_last_uid)) + new_max_uid = server.l10n_it_last_uid + for uid in data[0].split(): + if int(uid) <= server.l10n_it_last_uid: + # We get always minimum 1 message. If no new message, we receive the newest already managed. + continue + + result, data = imap_server.uid('fetch', uid, '(RFC822)') + + if not data[0]: + continue + message = data[0][1] + + # To leave the mail in the state in which they were. + if "Seen" not in data[1].decode("utf-8"): + imap_server.uid('STORE', uid, '+FLAGS', '\\Seen') + else: + imap_server.uid('STORE', uid, '-FLAGS', '\\Seen') + + # See details in message_process() in mail_thread.py + if isinstance(message, xmlrpclib.Binary): + message = bytes(message.data) + if isinstance(message, str): + message = message.encode('utf-8') + msg_txt = email.message_from_bytes(message) + + try: + self._attachment_invoice(msg_txt) + new_max_uid = max(new_max_uid, int(uid)) + except Exception: + _logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True) + failed += 1 + self._cr.commit() + count += 1 + server.write({'l10n_it_last_uid': new_max_uid}) + _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed) + except Exception: + _logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True) + finally: + if imap_server: + imap_server.close() + imap_server.logout() + server.write({'date': fields.Datetime.now()}) + return super(FetchmailServer, self.filtered(lambda s: not s.l10n_it_is_pec)).fetch_mail() + + def _attachment_invoice(self, msg_txt): + body, attachments = self.env['mail.thread']._message_extract_payload(msg_txt) + from_address = tools.decode_smtp_header(msg_txt.get('from')) + for attachment in attachments: + split_attachment = attachment.fname.rpartition('.') + if len(split_attachment) < 3: + _logger.info('E-invoice filename not compliant: %s', attachment.fname) + continue + attachment_name = split_attachment[0] + attachment_ext = split_attachment[2] + split_underscore = attachment_name.rsplit('_', 2) + if len(split_underscore) < 2: + _logger.info('E-invoice filename not compliant: %s', attachment.fname) + continue + + if attachment_ext != 'zip': + if split_underscore[1] in ['RC', 'NS', 'MC', 'MT', 'EC', 'SE', 'NE', 'DT']: + # we have a receipt + self._message_receipt_invoice(split_underscore[1], attachment) + elif re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname): + # we have a new E-invoice + self._create_invoice_from_mail(attachment.content, attachment.fname, from_address) + else: + if split_underscore[1] == 'AT': + # Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito + self._message_AT_invoice(attachment) + else: + _logger.info('New E-invoice in zip file: %s', attachment.fname) + self._create_invoice_from_mail_with_zip(attachment, from_address) + + def _create_invoice_from_mail(self, att_content, att_name, from_address): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1): + # invoice already exist + _logger.info('E-invoice already exist: %s', att_name) + return + + invoice_attachment = self.env['ir.attachment'].create({ + 'name': att_name, + 'datas': base64.encodestring(att_content), + 'datas_fname': att_name, + 'type': 'binary', + }) + + invoice = self.env['account.invoice']._import_xml_invoice(att_content, invoice_attachment) + invoice.l10n_it_send_state = "new" + invoice.source_email = from_address + self._cr.commit() + + _logger.info('New E-invoice: %s', att_name) + + + def _create_invoice_from_mail_with_zip(self, attachment_zip, from_address): + with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z: + for att_name in z.namelist(): + if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1): + # invoice already exist + _logger.info('E-invoice in zip file (%s) already exist: %s', attachment_zip.fname, att_name) + continue + att_content = z.open(att_name).read() + + self._create_invoice_from_mail(att_content, att_name, from_address) + + def _message_AT_invoice(self, attachment_zip): + with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z: + for attachment_name in z.namelist(): + split_name_attachment = attachment_name.rpartition('.') + if len(split_name_attachment) < 3: + continue + split_underscore = split_name_attachment[0].rsplit('_', 2) + if len(split_underscore) < 2: + continue + if split_underscore[1] == 'AT': + attachment = z.open(attachment_name).read() + _logger.info('New AT receipt for: %s', split_underscore[0]) + try: + tree = etree.fromstring(attachment) + except: + _logger.info('Error in decoding new receipt file: %s', attachment_name) + return + + elements = tree.xpath('//NomeFile', namespaces=tree.nsmap) + if elements and elements[0].text: + filename = elements[0].text + else: + return + + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename)]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', filename) + return + + related_invoice.l10n_it_send_state = 'failed_delivery' + info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//MessageId', '//PecMessageId', '//Note']) + related_invoice.message_post( + body=(_("ES certify that it has received the invoice and that the file \ + could not be delivered to the addressee. <br/>%s") % (info)) + ) + + def _message_receipt_invoice(self, receipt_type, attachment): + try: + tree = etree.fromstring(attachment.content) + except: + _logger.info('Error in decoding new receipt file: %s', attachment.fname) + return {} + + elements = tree.xpath('//NomeFile', namespaces=tree.nsmap) + if elements and elements[0].text: + filename = elements[0].text + else: + return {} + + if receipt_type == 'RC': + # Delivery receipt + # This is the receipt sent by the ES to the transmitting subject to communicate + # delivery of the file to the addressee + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'delivered' + info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//DataOraConsegna', '//Note']) + related_invoice.message_post( + body=(_("E-Invoice is delivery to the destinatory:<br/>%s") % (info)) + ) + + elif receipt_type == 'NS': + # Rejection notice + # This is the receipt sent by the ES to the transmitting subject if one or more of + # the checks carried out by the ES on the file received do not have a successful result. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'invalid' + error = self._return_error_xml(tree) + related_invoice.message_post( + body=(_("Errors in the E-Invoice :<br/>%s") % (error)) + ) + activity_vals = { + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id + } + related_invoice.activity_schedule(summary='Rejection notice', **activity_vals) + + elif receipt_type == 'MC': + # Failed delivery notice + # This is the receipt sent by the ES to the transmitting subject if the file is not + # delivered to the addressee. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'sent')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + info = self._return_multi_line_xml(tree, [ + '//IdentificativoSdI', + '//DataOraRicezione', + '//Descrizione', + '//MessageId', + '//Note']) + related_invoice.message_post( + body=(_("The E-invoice is not delivered to the addressee. The Exchange System is\ + unable to deliver the file to the Public Administration. The Exchange System will\ + contact the PA to report the problem and request that they provide a solution. \ + During the following 15 days, the Exchange System will try to forward the FatturaPA\ + file to the Administration in question again. More informations:<br/>%s") % (info)) + ) + + elif receipt_type == 'NE': + # Outcome notice + # This is the receipt sent by the ES to the invoice sender to communicate the result + # (acceptance or refusal of the invoice) of the checks carried out on the document by + # the addressee. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), + ('l10n_it_send_state', '=', 'delivered')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + elements = tree.xpath('//Esito', namespaces=tree.nsmap) + if elements and elements[0].text: + if elements[0].text == 'EC01': + related_invoice.l10n_it_send_state = 'delivered_accepted' + elif elements[0].text == 'EC02': + related_invoice.l10n_it_send_state = 'delivered_refused' + + info = self._return_multi_line_xml(tree, + ['//Esito', + '//Descrizione', + '//IdentificativoSdI', + '//DataOraRicezione', + '//DataOraConsegna', + '//Note' + ]) + related_invoice.message_post( + body=(_("Outcome notice: %s<br/>%s") % (related_invoice.l10n_it_send_state, info)) + ) + if related_invoice.l10n_it_send_state == 'delivered_refused': + activity_vals = { + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id + } + related_invoice.activity_schedule(summary='Outcome notice: Refused', **activity_vals) + + # elif receipt_type == 'MT': + # Metadata file + # This is the file sent by the ES to the addressee together with the invoice file, + # containing the main reference data of the file useful for processing, including + # the IdentificativoSDI. + # Useless for Odoo + + elif receipt_type == 'DT': + # Deadline passed notice + # This is the receipt sent by the ES to both the invoice sender and the invoice + # addressee to communicate the expiry of the maximum term for communication of + # acceptance/refusal. + related_invoice = self.env['account.invoice'].search([ + ('l10n_it_einvoice_name', '=', filename), ('l10n_it_send_state', '=', 'delivered')]) + if not related_invoice: + _logger.info('Error: invoice not found for receipt file: %s', attachment.fname) + return + related_invoice.l10n_it_send_state = 'delivered_expired' + info = self._return_multi_line_xml(tree, [ + '//Descrizione', + '//IdentificativoSdI', + '//Note']) + related_invoice.message_post( + body=(_("Expiration of the maximum term for communication of acceptance/refusal:\ + %s<br/>%s") % (filename, info)) + ) + + def _return_multi_line_xml(self, tree, element_tags): + output_str = "<ul>" + + for element_tag in element_tags: + elements = tree.xpath(element_tag, namespaces=tree.nsmap) + if not elements: + continue + for element in elements: + text = " ".join(element.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (element.tag, text) + return output_str + "</ul>" + + def _return_error_xml(self, tree): + output_str = "<ul>" + + elements = tree.xpath('//Errore', namespaces=tree.nsmap) + if not elements: + return + for element in elements: + descrizione = " ".join(element[1].text.split()) + if descrizione: + output_str += "<li>Errore %s: %s</li>" % (element[0].text, descrizione) + return output_str + "</ul>" diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py new file mode 100644 index 00000000000..a0cce53b9a5 --- /dev/null +++ b/addons/l10n_it_edi/models/res_company.py @@ -0,0 +1,110 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +TAX_SYSTEM = [ + ("RF01", "[RF01] Ordinario"), + ("RF02", "[RF02] Contribuenti minimi (art.1, c.96-117, L. 244/07)"), + ("RF04", "[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)"), + ("RF05", "[RF05] Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)"), + ("RF06", "[RF06] Commercio fiammiferi (art.74, c.1, DPR 633/72)"), + ("RF07", "[RF07] Editoria (art.74, c.1, DPR 633/72)"), + ("RF08", "[RF08] Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)"), + ("RF09", "[RF09] Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)"), + ("RF10", "[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)"), + ("RF11", "[RF11] Agenzie viaggi e turismo (art.74-ter, DPR 633/72)"), + ("RF12", "[RF12] Agriturismo (art.5, c.2, L. 413/91)"), + ("RF13", "[RF13] Vendite a domicilio (art.25-bis, c.6, DPR 600/73)"), + ("RF14", "[RF14] Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)"), + ("RF15", "[RF15] Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)"), + ("RF16", "[RF16] IVA per cassa P.A. (art.6, c.5, DPR 633/72)"), + ("RF17", "[RF17] IVA per cassa (art. 32-bis, DL 83/2012)"), + ("RF18", "[RF18] Altro"), + ("RF19", "[RF19] Regime forfettario (art.1, c.54-89, L. 190/2014)"), +] + +class ResCompany(models.Model): + _name = 'res.company' + _inherit = 'res.company' + + l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16, related='partner_id.l10n_it_codice_fiscale', + store=True, readonly=False, help="Fiscal code of your company") + l10n_it_tax_system = fields.Selection(selection=TAX_SYSTEM, string="Tax System", + help="Please select the Tax system to which you are subjected.") + + # PEC server + l10n_it_mail_pec_server_id = fields.Many2one('ir.mail_server', string="Server PEC", + help="Configure your PEC-mail server to send electronic invoices.") + l10n_it_address_recipient_fatturapa = fields.Char(string="Government PEC-mail", + help="Enter Government PEC-mail address. Ex: sdi01@pec.fatturapa.it") + l10n_it_address_send_fatturapa = fields.Char(string="Company PEC-mail", + help="Enter your company PEC-mail address. Ex: yourcompany@pec.mail.it") + + + # Economic and Administrative Index + l10n_it_has_eco_index = fields.Boolean(default=False, + help="The seller/provider is a company listed on the register of companies and as\ + such must also indicate the registration data on all documents (art. 2250, Italian\ + Civil Code)") + l10n_it_eco_index_office = fields.Many2one('res.country.state', domain="[('country_id','=','IT')]", + string="Province of the register-of-companies office") + l10n_it_eco_index_number = fields.Char(string="Number in register of companies", size=20, + help="This field must contain the number under which the\ + seller/provider is listed on the register of companies.") + l10n_it_eco_index_share_capital = fields.Float(default="0.0", size=15, string="Share capital actually paid up", + help="Mandatory if the seller/provider is a company with share\ + capital (SpA, SApA, Srl), this field must contain the amount\ + of share capital actually paid up as resulting from the last\ + financial statement") + l10n_it_eco_index_sole_shareholder = fields.Selection( + [ + ("NO", "Not a limited liability company"), + ("SU", "Socio unico"), + ("SM", "Più soci")], + string="Shareholder") + l10n_it_eco_index_liquidation_state = fields.Selection( + [ + ("LS", "The company is in a state of liquidation"), + ("LN", "The company is not in a state of liquidation")], + string="Liquidation state") + + + # Tax representative + l10n_it_has_tax_representative = fields.Boolean(default=False, + help="The seller/provider is a non-resident subject which\ + carries out transactions in Italy with relevance for VAT\ + purposes and which takes avail of a tax representative in\ + Italy") + l10n_it_tax_representative_partner_id = fields.Many2one('res.partner', string='Tax representative partner') + + @api.constrains('l10n_it_has_eco_index', + 'l10n_it_eco_index_office', + 'l10n_it_eco_index_number', + 'l10n_it_eco_index_share_capital', + 'l10n_it_eco_index_sole_shareholder', + 'l10n_it_eco_index_liquidation_state') + def _check_eco_admin_index(self): + for record in self: + if not record.l10n_it_has_eco_index: + continue + if not record.l10n_it_eco_index_office\ + or not record.l10n_it_eco_index_number\ + or not record.l10n_it_eco_index_share_capital\ + or not record.l10n_it_eco_index_sole_shareholder\ + or not record.l10n_it_eco_index_liquidation_state: + raise ValidationError("All fields about the Economic and Administrative Index must be completed.") + + @api.constrains('l10n_it_has_tax_representative', + 'l10n_it_tax_representative_partner_id') + def _check_tax_representative(self): + for record in self: + if not record.l10n_it_has_tax_representative: + continue + if not record.l10n_it_tax_representative_partner_id: + raise ValidationError("You must select a tax representative.") + if not record.l10n_it_tax_representative_partner_id.vat: + raise ValidationError("Your tax representative partner must have a tax number.") + if not record.l10n_it_tax_representative_partner_id.country_id: + raise ValidationError("Your tax representative partner must have a country.") diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py new file mode 100644 index 00000000000..e0a78f24d58 --- /dev/null +++ b/addons/l10n_it_edi/models/res_partner.py @@ -0,0 +1,28 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + +class ResPartner(models.Model): + _name = 'res.partner' + _inherit = 'res.partner' + + l10n_it_pec_email = fields.Char(string="PEC e-mail") + l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16) + l10n_it_pa_index = fields.Char(string="PA index", + size=7, + help="Must contain the 6-character (or 7) code, present in the PA\ + Index in the information relative to the electronic invoicing service,\ + associated with the office which, within the addressee administration, deals\ + with receiving (and processing) the invoice.") + + _sql_constraints = [ + ('l10n_it_codice_fiscale', + "CHECK(l10n_it_codice_fiscale IS NULL OR LENGTH(l10n_it_codice_fiscale) >= 11)", + "Codice fiscale must have between 11 and 16 characters."), + + ('l10n_it_pa_index', + "CHECK(l10n_it_pa_index IS NULL OR LENGTH(l10n_it_pa_index) >= 6)", + "PA index must have between 6 and 7 characters."), + ] diff --git a/addons/l10n_it_edi/security/ir.model.access.csv b/addons/l10n_it_edi/security/ir.model.access.csv new file mode 100644 index 00000000000..22c0da9c159 --- /dev/null +++ b/addons/l10n_it_edi/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_it_ddt_manager","it_ddt manager","model_l10n_it_ddt","account.group_account_invoice",1,1,1,1
\ No newline at end of file diff --git a/addons/l10n_it_edi/tests/__init__.py b/addons/l10n_it_edi/tests/__init__.py new file mode 100644 index 00000000000..d7b285d123c --- /dev/null +++ b/addons/l10n_it_edi/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_generate diff --git a/addons/l10n_it_edi/tests/test_generate.py b/addons/l10n_it_edi/tests/test_generate.py new file mode 100644 index 00000000000..60592ade8c7 --- /dev/null +++ b/addons/l10n_it_edi/tests/test_generate.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import time + +from odoo import fields +from odoo.tests.common import TransactionCase, Form + +class TestItalianElectronicInvoice(TransactionCase): + def test_state(self): + f = Form(self.env['account.invoice']) + f.partner_id = self.env.ref('base.res_partner_12') + with f.invoice_line_ids.new() as l: + l.product_id = self.env.ref('product.product_product_3') + invoice = f.save() + + # I check that Initially customer invoice state is "Draft" + self.assertEqual(invoice.state, 'draft') + + # I called the "Confirm Draft Invoices" wizard + w = Form(self.env['account.invoice.confirm']).save() + # I clicked on Confirm Invoices Button + w.with_context( + active_model='account.invoice', + active_id=invoice.id, + active_ids=invoice.ids, + type='out_invoice', + ).invoice_confirm() + + # I check that customer invoice state is "Open" + self.assertEqual(invoice.state, 'open') + + # Electronic invoice must be present and have the same name as l10n_it_einvoice_name + self.assertEqual(invoice.l10n_it_einvoice_id.name, invoice.l10n_it_einvoice_name) diff --git a/addons/l10n_it_edi/views/l10n_it_view.xml b/addons/l10n_it_edi/views/l10n_it_view.xml new file mode 100644 index 00000000000..7ae8222bf79 --- /dev/null +++ b/addons/l10n_it_edi/views/l10n_it_view.xml @@ -0,0 +1,265 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="fetchmail_server_form_l10n_it" model="ir.ui.view"> + <field name="name">fetchmail.server.form.l10n.it</field> + <field name="model">fetchmail.server</field> + <field name="priority">20</field> + <field name="inherit_id" ref="fetchmail.view_email_server_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='date']" position="after"> + <field name="l10n_it_is_pec"/> + </xpath> + </data> + </field> + </record> + + <record id="account_tax_form_l10n_it" model="ir.ui.view"> + <field name="name">account.tax.form.l10n.it</field> + <field name="model">account.tax</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.view_tax_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//page" position="inside"> + <group> + <field name="l10n_it_vat_due_date"/> + <field name="l10n_it_has_exoneration" readonly="False"/> + <field name="l10n_it_kind_exoneration" attrs="{'invisible': [('l10n_it_has_exoneration', '=', False)]}"/> + <field name="l10n_it_law_reference" attrs="{'invisible': [('l10n_it_has_exoneration', '=', False)]}"/> + </group> + </xpath> + </data> + </field> + </record> + + <record id="res_partner_form_l10n_it" model="ir.ui.view"> + <field name="name">res.partner.form.l10n.it</field> + <field name="model">res.partner</field> + <field name="priority">20</field> + <field name="inherit_id" ref="base.view_partner_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='category_id']" position="after"> + <field name="l10n_it_pec_email" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + <field name="l10n_it_codice_fiscale" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + <field name="l10n_it_pa_index" attrs="{'invisible': [('parent_id', '!=', False)]}"/> + </xpath> + </data> + </field> + </record> + + <record id="res_company_form_l10n_it" model="ir.ui.view"> + <field name="name">res.company.form.l10n.it</field> + <field name="model">res.company</field> + <field name="priority">20</field> + <field name="inherit_id" ref="base.view_company_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//div[hasclass('o_address_format')]" position="after"> + <field name="l10n_it_mail_pec_server_id"/> + <field name="l10n_it_address_send_fatturapa"/> + <field name="l10n_it_address_recipient_fatturapa"/> + </xpath> + <xpath expr="//field[@name='vat']" position="after"> + <field name="l10n_it_codice_fiscale"/> + <field name="l10n_it_tax_system"/> + </xpath> + <xpath expr="//page" position="after"> + <page string="Electronic Invoicing"> + <group> + <separator string="Economic and Administrative Index" colspan="4"/> + <div colspan="4"> + The seller/provider is a company listed on the register of companies and as + such must also indicate the registration data on all documents (art. 2250, Italian + Civil Code) + </div> + <group> + <field name="l10n_it_has_eco_index" string="Company listed on the register of companies"/> + </group> + <group attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"> + <field name="l10n_it_eco_index_office"/> + <field name="l10n_it_eco_index_number"/> + <field name="l10n_it_eco_index_share_capital"/> + <field name="l10n_it_eco_index_sole_shareholder"/> + <field name="l10n_it_eco_index_liquidation_state"/> + </group> + </group> + <group> + <separator string="Tax representative" colspan="4"/> + <div colspan="4"> + The seller/provider is a non-resident subject which carries out transactions in Italy + with relevance for VAT purposes and which takes avail of a tax representative in Italy + </div> + <group> + <field name="l10n_it_has_tax_representative" string="Company have a tax representative"/> + </group> + <group attrs="{'invisible': [('l10n_it_has_tax_representative', '=', False)]}"> + <field name="l10n_it_tax_representative_partner_id"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="invoice_supplier_tree_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.supplier.tree.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_tree"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='type']" position="after"> + <field name="l10n_it_send_state" invisible="1" widget="label_selection" options="{'classes': {'to_send': 'default', 'invalid': 'danger', 'sent': 'warning', + 'delivered': 'success', 'delivered_accepted': 'success', 'delivered_refused': 'success', 'delivered_expired': 'success', 'failed_delivery': 'success'}}"/> + + <button icon="fa-paper-plane-o" class="btn-outline-warning disabled" aria-label="Sent" title="Sent" attrs="{'invisible': [('l10n_it_send_state', '!=', 'sent')]}"/> + <button icon="fa-exclamation-triangle" class="btn-outline-danger disabled" aria-label="Error" title="Error" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}"/> + <button icon="fa-check" class="btn-outline-success disabled" aria-label="Delivered" title="Delivered" attrs="{'invisible': [('l10n_it_send_state', 'not in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]}"/> + </xpath> + </data> + </field> + </record> + + <record id="invoice_kanban_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.kanban.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_kanban"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='currency_id']" position="after"> + <field name="l10n_it_send_state"/> + </xpath> + + <xpath expr="//div[hasclass('o_kanban_record_headings')]" position="inside"> + <i class="text-success fa fa-plus-circle" t-if="record.l10n_it_send_state.raw_value == 'new'"/> + <i class="text-warning fa fa-paper-plane-o" t-if="record.l10n_it_send_state.raw_value == 'sent'"/> + <i class="text-danger fa fa-exclamation-triangle" t-if="record.l10n_it_send_state.raw_value == 'invalid'"/> + <i class="text-success fa fa-check" t-if="['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'].indexOf(record.l10n_it_send_state.raw_value) >= 0" /> + </xpath> + </data> + </field> + </record> + + <record id="account_invoice_form_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.form.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//button[@name='preview_invoice']" position="after"> + <button name="invoice_validate" type="object" icon="fa-envelope-o" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}" class="btn btn-primary" string="Resend"/> + <button name="invoice_validate" type="object" icon="fa-envelope-o" attrs="{'invisible': ['|',('state', 'in', ['draft', 'cancel']),('l10n_it_send_state', 'not in', ['to_send', False])]}" class="btn btn-primary" string="Send"/> + </xpath> + <xpath expr="//field[@name='type']" position="before"> + <div class="alert alert-success" role="alert" attrs="{'invisible': [('l10n_it_send_state', 'not in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]}"> + <i class="fa fa-check" aria-label="Delivered" title="Delivered"></i> <field name="l10n_it_send_state" readonly="1"/> + </div> + <div class="alert alert-warning" role="alert" attrs="{'invisible': [('l10n_it_send_state', '!=', 'sent')]}"> + <i class="fa fa-paper-plane-o"/> E-Invoice sent, waiting for a response + </div> + <div class="alert alert-danger" role="alert" attrs="{'invisible': [('l10n_it_send_state', '!=', 'invalid')]}"> + <i class="fa fa-exclamation-triangle"/> E-Invoice check failed. You can modify the invoice, and resend it. + </div> + </xpath> + <xpath expr="//page[@name='other_info']" position="after"> + <page string="Electronic Invoicing"> + <group> + <group> + <field name="l10n_it_stamp_duty"/> + </group> + <group> + <field name="l10n_it_ddt_id"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="account_invoice_supplier_form_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.supplier.form.l10n.it</field> + <field name="model">account.invoice</field> + <field name="priority">20</field> + <field name="inherit_id" ref="account.invoice_supplier_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='date_due']" position="replace"> + <field name="date_due" attrs="{'readonly': [('state', 'in', ['open', 'in_payment', 'paid'])]}" force_save="1"/> + </xpath> + <xpath expr="//page[@name='other_info']" position="after"> + <page string="Electronic Invoicing"> + <group> + <group> + <field name="l10n_it_stamp_duty"/> + </group> + </group> + </page> + </xpath> + </data> + </field> + </record> + + <record id="view_account_invoice_filter_l10n_it" model="ir.ui.view"> + <field name="name">account.invoice.select.l10n.it</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.view_account_invoice_filter"/> + <field name="arch" type="xml"> + <xpath expr="//filter[@name='late']" position="after"> + <separator/> + <filter name="error" string="E-invoice error" domain="[('l10n_it_send_state', '=', 'invalid')]"/> + <filter name="sent" string="E-invoice sent" domain="[('l10n_it_send_state', '=', 'sent')]"/> + <filter name="no_sent" string="E-invoice to send" domain="[('l10n_it_send_state', 'in', ['to_send',False])]"/> + <filter name="delivered" string="E-invoice delivered" domain="[('l10n_it_send_state', 'in', ['delivered', 'delivered_accepted', 'delivered_refused', 'delivered_expired', 'failed_delivery'])]"/> + </xpath> + <xpath expr="//filter[@name='status']" position="after"> + <filter name="send_status" string="Send status" context="{'group_by':'l10n_it_send_state'}"/> + </xpath> + </field> + </record> + + <record id="l10n_it_ddt" model="ir.ui.view"> + <field name="name">ddt.form.l10n.it</field> + <field name="model">l10n_it.ddt</field> + <field name="arch" type="xml"> + <form> + <group> + <field name="name"/> + <field name="date"/> + </group> + </form> + </field> + </record> + + <record id="l10n_it_ddt_list_view" model="ir.ui.view"> + <field name="name">l10n_it.ddt.list.view</field> + <field name="model">l10n_it.ddt</field> + <field name="arch" type="xml"> + <tree> + <field name="name"/> + <field name="date"/> + </tree> + </field> + </record> + + <record id="action_ddt_account" model="ir.actions.act_window"> + <field name="name">Transport Document</field> + <field name="res_model">l10n_it.ddt</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="l10n_it_ddt_list_view"/> + </record> + + <menuitem + name="DDT" + parent="account.account_account_menu" + action="action_ddt_account" + id="menu_action_ddt_account" + sequence="15" + groups="base.group_no_one"/> +</odoo> diff --git a/addons/l10n_mx/data/account_data.xml b/addons/l10n_mx/data/account_data.xml index 782597ee8ea..8176c32b5f3 100644 --- a/addons/l10n_mx/data/account_data.xml +++ b/addons/l10n_mx/data/account_data.xml @@ -11,6 +11,10 @@ <field name="name">IVA 16% </field> </record> + <record id="tax_group_iva_8" model="account.tax.group"> + <field name="name">IVA 8%</field> + </record> + <record id="tax_group_iva_ret_4" model="account.tax.group"> <field name="name">IVA Retencion 4%</field> </record> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index adda588dcbd..13f78068494 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -30,6 +30,10 @@ <field name="name">DIOT: 0%</field> <field name="applicability">taxes</field> </record> + <record id="tag_diot_8" model="account.account.tag"> + <field name="name">DIOT: 8%</field> + <field name="applicability">taxes</field> + </record> <record id="tag_diot_ret" model="account.account.tag"> <field name="name">DIOT: Retención</field> <field name="applicability">taxes</field> @@ -190,5 +194,35 @@ <field name="cash_basis_account_id" ref="cuenta118_01"/> <field name="tag_ids" eval="[(6,0,[ref('tag_diot_16')])]"/> </record> + + <record id="tax16" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) COMPRAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="account_id" ref="cuenta119_01"/> + <field name="refund_account_id" ref="cuenta119_01"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account_id" ref="cuenta118_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_diot_8')])]"/> + </record> + + <record id="tax17" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) VENTAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="account_id" ref="cuenta209_01"/> + <field name="refund_account_id" ref="cuenta209_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account_id" ref="cuenta208_01"/> + </record> </data> </odoo> diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index a566d1e82ac..89b99d5ae82 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -231,7 +231,9 @@ <td align="center" style="min-width: 590px;"> <t t-set="access_action" t-value="record.with_context(force_website=True).get_access_action()"/> <t t-set="is_online" t-value="access_action and access_action['type'] == 'ir.actions.act_url'"/> - <t t-set="access_url" t-value="is_online and record._get_share_url(redirect=True, signup_partner=True) or ''"/> + <t t-set="base_url" t-value="record.get_base_url()"/> + <t t-set="share_url" t-value="record._get_share_url(redirect=True, signup_partner=True)"/> + <t t-set="access_url" t-value="is_online and share_url and base_url + share_url or ''"/> <t t-set="access_name"> View <t t-esc="model_description or 'document'"/> </t> diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 51e87a86982..1418c066b2e 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -553,7 +553,7 @@ class Message(models.Model): 'model': message.model, 'last_message_date': message.date, 'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg', - 'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids) + 'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids.sudo()) } failures_infos.append(info) return failures_infos diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 5892de5137b..df8fd6845ce 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -740,7 +740,9 @@ class MailThread(models.AbstractModel): access_link = self._notify_get_action_link('view') if message.model: - model_name = self.env['ir.model']._get(message.model).display_name + model = self.env['ir.model'].with_context( + lang=self.env.context.get('lang', self.env.user.lang)) + model_name = model._get(message.model).display_name view_title = _('View %s') % model_name else: view_title = _('View') diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 330e56b77b4..72971fbeee9 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -90,6 +90,7 @@ var Chatter = Widget.extend({ this._$topbar = this.$('.o_chatter_topbar'); if(!this._disableAttachmentBox) { this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', { + displayCounter: !!this.fields.thread, count: this.record.data.message_attachment_count || 0, })); } @@ -362,7 +363,9 @@ var Chatter = Widget.extend({ if (this._isAttachmentBoxOpen) { this._fetchAttachments().then(this._openAttachmentBox.bind(this)); } - this.trigger_up('reload', { fieldNames: ['message_attachment_count'] }); + if (this.fields.thread) { + this.trigger_up('reload', { fieldNames: ['message_attachment_count'] }); + } }, /** * @private @@ -490,7 +493,9 @@ var Chatter = Widget.extend({ }) .then(function () { self._reloadAttachmentBox(); - self.fields.thread.removeAttachments([ev.data.attachmentId]); + if (self.fields.thread) { + self.fields.thread.removeAttachments([ev.data.attachmentId]); + } self.trigger_up('reload'); }); } diff --git a/addons/mail/static/src/js/thread_widget.js b/addons/mail/static/src/js/thread_widget.js index 2e2b7e222c6..cbac2c3b417 100644 --- a/addons/mail/static/src/js/thread_widget.js +++ b/addons/mail/static/src/js/thread_widget.js @@ -103,6 +103,7 @@ var ThreadWidget = Widget.extend({ this._messageSeenPopover.popover('hide'); } this._destroyOpenSeenPopoverIDs(); + this._super(); }, /** * @param {mail.model.AbstractThread} thread the thread to render. diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index 7f8629f1bae..7fe1eeebd77 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -105,11 +105,17 @@ </t> + <!-- + Chatter attachment paperclip button. + + @param {boolean} [displayCounter] + @param {integer} [count] + --> <t t-name="mail.chatter.Attachment.Button"> <button class="btn btn-link o_chatter_button_attachment" title="View all the attachments of the current record"> <span> <i class="fa fa-paperclip"/> - <span class="o_chatter_attachment_button_count"> + <span t-if="displayCounter" class="o_chatter_attachment_button_count"> <t t-esc="count"/> </span> </span> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 943059eb6d9..601576c4bf0 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -716,6 +716,12 @@ class MassMailing(models.Model): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() + res_ids = self.get_recipients() + except_mailed = self.env['mail.mail.statistics'].search([ + ('model', '=', self.mailing_model_real), + ('res_id', 'in', res_ids), + ('exception', '!=', False), + ('mass_mailing_id', '=', self.id)]).unlink() self.write({'state': 'in_queue'}) def action_view_sent(self): diff --git a/addons/note/static/src/js/systray_activity_menu.js b/addons/note/static/src/js/systray_activity_menu.js index e2e484ca10f..f8a5e8e288e 100644 --- a/addons/note/static/src/js/systray_activity_menu.js +++ b/addons/note/static/src/js/systray_activity_menu.js @@ -118,7 +118,7 @@ ActivityMenu.include({ _onNoteDateTimeSetClick: function (ev) { ev.preventDefault(); ev.stopPropagation(); - this.noteDateTimeWidget.$input.click(); + this.noteDateTimeWidget.$input.select(); }, /** * Saving note (quick create) and updating activity preview diff --git a/addons/note/static/tests/systray_activity_menu_tests.js b/addons/note/static/tests/systray_activity_menu_tests.js index 8c4e2dde60a..151af26dfbc 100644 --- a/addons/note/static/tests/systray_activity_menu_tests.js +++ b/addons/note/static/tests/systray_activity_menu_tests.js @@ -108,7 +108,6 @@ QUnit.test('note activity menu widget: create note from activity menu', function // creating quick note with date testUtils.dom.click(activityMenu.$('.o_note_show')); activityMenu.$('input.o_note_input').val("New Note"); - testUtils.dom.click(activityMenu.$('.o_note_set_datetime')); testUtils.dom.click(activityMenu.$(".o_note_save")); assert.strictEqual(activityMenu.$('.o_notification_counter').text(), '2', "should increment activity notification counter after creating a second note"); diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 82d519c56d5..0d3710df90f 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -501,13 +501,11 @@ class PosOrder(models.Model): aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) - # Reconcile returns first - # to avoid mixing up the credit of a payment and the credit of a return - # in the receivable account - aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit)) try: - aml_returns.reconcile() - (aml - aml_returns).reconcile() + # Cash returns will be well reconciled + # Whereas freight returns won't be + # "c'est la vie..." + aml.reconcile() except Exception: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index f1034572ecc..885528fb9bf 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -184,6 +184,8 @@ class PosSession(models.Model): journals = Journal.with_context(ctx).search([('type', '=', 'cash')]) if not journals: journals = Journal.with_context(ctx).search([('journal_user', '=', True)]) + if not journals: + raise ValidationError(_("No payment method configured! \nEither no Chart of Account is installed or no payment method is configured for this POS.")) journals.sudo().write({'journal_user': True}) pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index a0626326b62..b1d99e2e199 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -383,7 +383,7 @@ var Gui = core.Class.extend({ numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; - var newbuf_float = field_utils.parse.float(newbuf); + var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf); var decimal_point = _t.database.parameters.decimal_point; if (input === decimal_point) { @@ -403,7 +403,9 @@ var Gui = core.Class.extend({ newbuf = newbuf.substring(1,newbuf.length); } } else if (input === '-') { - if ( newbuf[0] === '-' ) { + if (options.firstinput) { + newbuf = '-'; + } else if ( newbuf[0] === '-' ) { newbuf = newbuf.substring(1,newbuf.length); } else { newbuf = '-' + newbuf; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 4420434229a..fb1b0550a46 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -815,8 +815,8 @@ var ProductListWidget = PosBaseWidget.extend({ }; this.keypress_product_handler = function(ev){ - if (ev.which != 13 && ev.which != 32) { - // Key is not space or enter + // React only to SPACE to avoid interfering with warcode scanner which sends ENTER + if (ev.which != 32) { return; } ev.preventDefault(); diff --git a/addons/point_of_sale/views/pos_templates.xml b/addons/point_of_sale/views/pos_templates.xml index 88a108bc12c..7728eb04048 100644 --- a/addons/point_of_sale/views/pos_templates.xml +++ b/addons/point_of_sale/views/pos_templates.xml @@ -1,5 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> +<template id="pos_assets_backend" name="POS UI Backend Assets" inherit_id="web.assets_backend" primary="True"> + <xpath expr="//script[contains(@src, '/mail/static/src/js/services/mail_service.js')]" position="replace"></xpath> +</template> + <template id="index" name="POS Index"><!DOCTYPE html> <html> <head> @@ -31,7 +35,7 @@ </script> <t t-call-assets="web.assets_common" t-css="false"/> - <t t-call-assets="web.assets_backend" t-css="false"/> + <t t-call-assets="point_of_sale.pos_assets_backend" t-css="false"/> <t t-call-assets="web_editor.summernote" t-css="false"/> <t t-call-assets="web_editor.assets_editor" t-css="false"/> <t t-call-assets="point_of_sale.assets"/> diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py index 99d8f18a266..09dc6ec9c01 100644 --- a/addons/portal/models/portal_mixin.py +++ b/addons/portal/models/portal_mixin.py @@ -35,6 +35,16 @@ class PortalMixin(models.AbstractModel): self.sudo().write({'access_token': str(uuid.uuid4())}) return self.access_token + @api.multi + def get_base_url(self): + """Get the base URL for the current model. + + Defined here to be overriden by website specific models. + The method has to be public because it is called from mail templates. + """ + self.ensure_one() + return self.env['ir.config_parameter'].sudo().get_param('web.base.url') + def _get_share_url(self, redirect=False, signup_partner=False, pid=None): """ Build the url of the record that will be sent by mail and adds additional parameters such as diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py index 5905df4eed4..8474f2c810c 100644 --- a/addons/portal/wizard/portal_share.py +++ b/addons/portal/wizard/portal_share.py @@ -10,9 +10,10 @@ class PortalShare(models.TransientModel): @api.model def default_get(self, fields): result = super(PortalShare, self).default_get(fields) + record = self.env[result['res_model']].browse(result['res_id']) result['res_model'] = self._context.get('active_model') result['res_id'] = self._context.get('active_id') - result['share_link'] = self.env[result['res_model']].browse(result['res_id'])._get_share_url(redirect=True) + result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True) return result res_model = fields.Char('Related Document Model', required=True) @@ -24,12 +25,11 @@ class PortalShare(models.TransientModel): @api.depends('res_model', 'res_id') def _compute_share_link(self): - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for rec in self: res_model = self.env[rec.res_model] if isinstance(res_model, self.pool['portal.mixin']): record = res_model.browse(rec.res_id) - rec.share_link = base_url + record._get_share_url(redirect=True) + rec.share_link = record.get_base_url() + record._get_share_url(redirect=True) @api.depends('res_model', 'res_id') def _compute_access_warning(self): @@ -52,7 +52,7 @@ class PortalShare(models.TransientModel): partner_ids = self.partner_ids.filtered(lambda x: x.user_ids) # if partner already user or record has access token send common link in batch to all user for partner in self.partner_ids: - share_link = active_record._get_share_url(redirect=True, pid=partner.id) + share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id) active_record.with_context(mail_post_autofollow=True).message_post_with_view(template, values={'partner': partner, 'note': self.note, 'record': active_record, 'share_link': share_link}, @@ -67,7 +67,7 @@ class PortalShare(models.TransientModel): share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id] active_record.with_context(mail_post_autofollow=True).message_post_with_view(template, values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link }, + 'share_link': share_link}, subject=_("You are invited to access %s" % active_record.display_name), subtype_id=note.id, notif_layout='mail.mail_notification_light', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 167d7f7b5ad..27b6d4b21f9 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -268,11 +268,18 @@ class Project(models.Model): """ copy and map tasks from old to new project """ tasks = self.env['project.task'] # We want to copy archived task, but do not propagate an active_test context key - task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)]).ids + task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)], order='parent_id').ids + old_to_new_tasks = {} for task in self.env['project.task'].browse(task_ids): # preserve task name and stage, normally altered during copy defaults = self._map_tasks_default_valeus(task) - tasks += task.copy(defaults) + if task.parent_id: + # set the parent to the duplicated task + defaults['parent_id'] = old_to_new_tasks[task.parent_id.id] + new_task = task.copy(defaults) + old_to_new_tasks[task.id] = new_task.id + tasks += new_task + return self.browse(new_project_id).write({'tasks': [(6, 0, tasks.ids)]}) @api.multi @@ -283,6 +290,8 @@ class Project(models.Model): if not default.get('name'): default['name'] = _("%s (copy)") % (self.name) project = super(Project, self).copy(default) + if self.subtask_project_id == self: + project.subtask_project_id = project for follower in self.message_follower_ids: project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) if 'tasks' not in default: @@ -663,6 +672,7 @@ class Task(models.Model): # force some parent values, if needed if 'parent_id' in vals and vals['parent_id']: vals.update(self._subtask_values_from_parent(vals['parent_id'])) + context.pop('default_parent_id', None) # for default stage if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') diff --git a/addons/project/static/src/js/tour.js b/addons/project/static/src/js/tour.js index 82ed66a15b9..951390455b9 100644 --- a/addons/project/static/src/js/tour.js +++ b/addons/project/static/src/js/tour.js @@ -73,11 +73,6 @@ tour.register('project_tour', { }, { trigger: ".o_kanban_record .oe_kanban_content", extra_trigger: '.o_kanban_project_tasks', - content: _t("Click on a card to get the details of the task."), - position: "bottom", -}, { - trigger: ".o_kanban_record .oe_kanban_content", - extra_trigger: '.o_kanban_project_tasks', content: _t("Click on the card to write more information about it and collaborate with your coworkers."), position: "bottom", }, { diff --git a/addons/purchase_requisition/data/purchase_requisition_data.xml b/addons/purchase_requisition/data/purchase_requisition_data.xml index 84194b8d89b..6364427724c 100644 --- a/addons/purchase_requisition/data/purchase_requisition_data.xml +++ b/addons/purchase_requisition/data/purchase_requisition_data.xml @@ -22,6 +22,7 @@ <field name="code">purchase.requisition.purchase.tender</field> <field name="prefix">TE</field> <field name="padding">5</field> + <field name="company_id" eval="False"></field> </record> <record id="seq_blanket_order" model="ir.sequence"> @@ -29,6 +30,7 @@ <field name="code">purchase.requisition.blanket.order</field> <field name="prefix">BO</field> <field name="padding">5</field> + <field name="company_id" eval="False"></field> </record> </data> </odoo> diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 15ebf47fa60..48a37020c96 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -867,7 +867,14 @@ class SaleOrder(models.Model): self.ensure_one() return '%s %s' % (self.type_name, self.name) + @api.multi def _get_share_url(self, redirect=False, signup_partner=False, pid=None): + """Override for sales order. + + If the SO is in a state where an action is required from the partner, + return the URL with a login token. Otherwise, return the URL with a + generic access token (no login). + """ self.ensure_one() if self.state not in ['sale', 'done']: auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id]) @@ -1433,7 +1440,6 @@ class SaleOrderLine(models.Model): result = {'warning': warning} if product.sale_line_warn == 'block': self.product_id = False - return result name = self.get_sale_order_line_multiline_description_sale(product) diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py index 7982d7c3e4d..c01ef48d96a 100644 --- a/addons/sale_crm/models/crm_lead.py +++ b/addons/sale_crm/models/crm_lead.py @@ -20,7 +20,7 @@ class CrmLead(models.Model): nbr = 0 company_currency = lead.company_currency or self.env.user.company_id.currency_id for order in lead.order_ids: - if order.state in ('draft', 'sent', 'sale'): + if order.state in ('draft', 'sent'): nbr += 1 if order.state not in ('draft', 'sent', 'cancel'): total += order.currency_id._convert( diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index 866890ff822..fb79fd5f976 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -46,7 +46,7 @@ class SaleOrderLine(models.Model): # Calculation of the margin for programmatic creation of a SO line. It is therefore not # necessary to call product_id_change_margin manually - if 'purchase_price' not in vals: + if 'purchase_price' not in vals and ('display_type' not in vals or not vals['display_type']): order_id = self.env['sale.order'].browse(vals['order_id']) product_id = self.env['product.product'].browse(vals['product_id']) product_uom_id = self.env['uom.uom'].browse(vals['product_uom']) diff --git a/addons/sale_quotation_builder/views/sale_portal_templates.xml b/addons/sale_quotation_builder/views/sale_portal_templates.xml index 93b41b50a76..a205c274781 100644 --- a/addons/sale_quotation_builder/views/sale_portal_templates.xml +++ b/addons/sale_quotation_builder/views/sale_portal_templates.xml @@ -9,6 +9,12 @@ <t t-if="line.product_id.product_tmpl_id.id not in product_tmpl_ids"> <t t-set="product_tmpl_ids" t-value="product_tmpl_ids + [line.product_id.product_tmpl_id.id]"/> <a t-att-id="line.id"/> + <div class="alert alert-info alert-dismissable mt16 css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button> + Product: <strong t-esc="line.product_id.name"/>: + this content will appear on the quotation only if this + product is not removed. + </div> <div t-att-class="'oe_no_empty' if line.website_description else 'oe_no_empty d-print-none'" t-field="line.website_description"/> </t> </t> diff --git a/addons/sale_stock/views/sale_stock_portal_template.xml b/addons/sale_stock/views/sale_stock_portal_template.xml index 9069b6c1e27..43dfdb779cf 100644 --- a/addons/sale_stock/views/sale_stock_portal_template.xml +++ b/addons/sale_stock/views/sale_stock_portal_template.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <template id="sale_order_portal_content_inherit_sale_stock" name="Orders Shipping Followup" inherit_id="sale.sale_order_portal_content"> - <xpath expr="//div[@id='shipping_address']" position="inside"> + <xpath expr="//div[@id='informations']" position="inside"> <t t-if="sale_order.picking_ids"> <div> <strong>Delivery Orders</strong> diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 6c45b06a508..0d6d015c2c5 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -258,7 +258,9 @@ class Product(models.Model): # TODO: Still optimization possible when searching virtual quantities ids = [] - for product in self.with_context(prefetch_fields=False).search([]): + # Order the search on `id` to prevent the default order on the product name which slows + # down the search because of the join on the translation table to get the translated names. + for product in self.with_context(prefetch_fields=False).search([], order='id'): if OPERATORS[operator](product[field], value): ids.append(product.id) return [('id', 'in', ids)] diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index a91a860dee9..a6958d6d14d 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -5,18 +5,14 @@ <t t-call="web.html_container"> <t t-call="web.external_layout"> <t t-set="o" t-value="o.with_context({'lang':o.partner_id.lang})" /> - <t t-if="o.move_lines and o.move_lines[0].partner_id" name="partner_header"> + <t t-set="partner" t-value="o.partner_id or (o.move_lines and o.move_lines[0].partner_id) or False"/> + <t t-if="partner" name="partner_header"> <t t-set="address"> - <div t-field="o.move_lines[0].partner_id" + <div t-esc="partner" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> </t> </t> - <t t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header"> - <t t-set="address"> - <div t-field="o.partner_id" - t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> - </t> - </t> + <div class="page"> <h2> <span t-field="o.name"/> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index cc4b96edb29..aeafa64444e 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -248,12 +248,9 @@ <group> <group> <field name="partner_id"/> - <button name="action_assign_partner" string="Assign Partner" type="object" help="Assign the partner to the Operations. Use only if the partner is manually changed!" - attrs="{'invisible': ['|', '|', ('move_line_exist', '=', False), ('state', 'not in', ('draft', 'assigned', 'confirmed')), ('picking_type_code', '=', 'internal')]}" - class="oe_link oe_edit_only"/> + <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> + <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> <field name="picking_type_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/> - <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/> <field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/> </group> <group> diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index f96a9f0f974..2d45899f142 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -265,7 +265,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=51, emp=71): # com runbot: 51 - 71 // test_mail only: 48 - 68 + with self.assertQueryCount(__system__=52, emp=72): # com runbot: 52 - 72 // test_mail only: 48 - 68 record.message_post( body='<p>Test Post Performances with an email ping</p>', partner_ids=self.customer.ids, diff --git a/addons/test_website/__init__.py b/addons/test_website/__init__.py new file mode 100644 index 00000000000..273ede25ea3 --- /dev/null +++ b/addons/test_website/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py new file mode 100644 index 00000000000..557722971d4 --- /dev/null +++ b/addons/test_website/__manifest__.py @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Website Test', + 'version': '1.0', + 'category': 'Hidden', + 'sequence': 9876, + 'summary': 'Website Test, mainly for module install/uninstall tests', + 'description': """This module contains tests related to website. Those are +contained in a separate module as we are testing module install/uninstall/upgrade +and we don't want to reload the website module every time, including it's possible +dependencies. Neither we want to add in website module some routes, views and +models which only purpose is to run tests.""", + 'depends': [ + 'website', + ], + 'data': [ + 'views/templates.xml', + 'data/test_website_data.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/addons/test_website/controllers/__init__.py b/addons/test_website/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/test_website/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/test_website/controllers/main.py b/addons/test_website/controllers/main.py new file mode 100644 index 00000000000..fad797d6984 --- /dev/null +++ b/addons/test_website/controllers/main.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request +from odoo.addons.portal.controllers.web import Home + + +class WebsiteTest(Home): + + @http.route('/test_view', type='http', auth="public", website=True) + def test_view(self, **kw): + return request.render('test_website.test_view') diff --git a/addons/test_website/data/test_website_data.xml b/addons/test_website/data/test_website_data.xml new file mode 100644 index 00000000000..df827d542a3 --- /dev/null +++ b/addons/test_website/data/test_website_data.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <record id="test_view" model="ir.ui.view"> + <field name="name">Test View</field> + <field name="type">qweb</field> + <field name="key">test_website.test_view</field> + <field name="arch" type="xml"> + <t name="Test View" priority="29" t-name="test_website.test_view"> + <t t-call="website.layout"> + <p>Test View</p> + <p>placeholder</p> + </t> + </t> + </field> + </record> + <record id="test_page_view" model="ir.ui.view"> + <field name="name">Test Page View</field> + <field name="type">qweb</field> + <field name="key">test_website.test_page_view</field> + <field name="arch" type="xml"> + <t name="Test Page View" priority="29" t-name="test_website.test_page_view"> + <t t-call="website.layout"> + <div id="oe_structure_test_website_page" class="oe_structure oe_empty"/> + <p>Test Page View</p> + <p>placeholder</p> + </t> + </t> + </field> + </record> + <record id="test_page" model="website.page"> + <field name="website_published">True</field> + <field name="url">/test_page_view</field> + <field name="view_id" ref="test_page_view"/> + </record> + <record id="test_view_to_be_t_called" model="ir.ui.view"> + <field name="name">Test View To Be t-called</field> + <field name="type">qweb</field> + <field name="key">test_website.test_view_to_be_t_called</field> + <field name="arch" type="xml"> + <t name="Test View To Be t-called" priority="29" t-name="test_website.test_view_to_be_t_called"> + <p>Test View To Be t-called</p> + <p>placeholder</p> + </t> + </field> + </record> + <template id="test_view_child_broken" inherit_id="test_website.test_view" active="False"> + <xpath expr="//p[last()]" position="replace"> + <p>Test View Child Broken</p> + <p>placeholder</p> + </xpath> + </template> + </data> +</odoo> diff --git a/addons/test_website/static/src/js/reset_views.test.js b/addons/test_website/static/src/js/reset_views.test.js new file mode 100644 index 00000000000..957cda6d594 --- /dev/null +++ b/addons/test_website/static/src/js/reset_views.test.js @@ -0,0 +1,103 @@ +odoo.define('test_website.reset_views', function (require) { +'use strict'; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); + +tour.register('test_reset_page_view_complete_flow_part1', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + // 1. Edit the page through Edit Mode, it will COW the view + { + content: "enter edit mode", + trigger: "a[data-action=edit]" + }, + { + content: "drop a snippet", + trigger: "#oe_snippets .oe_snippet:has(.s_cover) .oe_snippet_thumbnail", + // id starting by 'oe_structure..' will actually create an inherited view + run: "drag_and_drop #oe_structure_test_website_page", + }, + { + content: "save the page", + extra_trigger: '#oe_structure_test_website_page .s_cover', + trigger: "#web_editor-top-edit button[data-action=save]", + }, + // 2. Edit that COW'd view in the HTML editor to break it. + { + content: "open customize menu", + extra_trigger: "body:not(.editor_enable)", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("placeholder")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n'); + }, + }, + { + content: "save the html editor", + extra_trigger: '.ace_content:contains("not.exist")', + trigger: ".o_ace_view_editor button[data-action=save]", + } + + // 3. Reset the broken view + ] +); + +tour.register('test_reset_page_view_complete_flow_part2', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + { + content: "check that the view got fixed", + trigger: 'p:containsExact("Test Page View")', + run: function () {}, // it's a check + }, + { + content: "check that the inherited COW view is still there (created during edit mode)", + trigger: '#oe_structure_test_website_page .s_cover', + run: function () {}, // it's a check + }, + //4. Now break the inherited view created when dropping a snippet + { + content: "open customize menu", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "select oe_structure view", + trigger: '#ace-view-list', + run: function () { + var viewId = $('#ace-view-list option:contains("oe_structure")').val(); + $('#ace-view-list').val(viewId).trigger('change'); + }, + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n'); + }, + }, + { + content: "save the html editor", + trigger: ".o_ace_view_editor button[data-action=save]", + } + ] +); + +}); diff --git a/addons/test_website/tests/__init__.py b/addons/test_website/tests/__init__.py new file mode 100644 index 00000000000..4c32d5f1959 --- /dev/null +++ b/addons/test_website/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_reset_views diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py new file mode 100644 index 00000000000..047b1abe3cd --- /dev/null +++ b/addons/test_website/tests/test_reset_views.py @@ -0,0 +1,102 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import re + +import odoo.tests +from odoo.tools import mute_logger + + +def break_view(view, fr='<p>placeholder</p>', to='<p t-field="not.exist"/>'): + view.arch = view.arch.replace(fr, to) + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestWebsiteResetViews(odoo.tests.HttpCase): + def do_test(self, name): + self.browser_js( + "/", + "odoo.__DEBUG__.services['web_tour.tour'].run('%s')" % name, + "odoo.__DEBUG__.services['web_tour.tour'].tours.%s.ready" % name, + login="admin" + ) + + def fix_it(self, page): + self.authenticate("admin", "admin") + resp = self.url_open(page) + self.assertEqual(resp.status_code, 500, "Waiting 500") + self.assertTrue('<button id="reset_templates_button"' in resp.text) + data = {'templates': [self.find_template(resp)], 'redirect': page} + resp = self.url_open('/website/reset_templates', data) + self.assertEqual(resp.status_code, 200, "Waiting 200") + + def find_template(self, response): + find = re.search(r'<input.*type="checkbox".*name="templates".*value="([0-9]+)?"', response.text) + return find and find.group(1) + + def setUp(self): + super(TestWebsiteResetViews, self).setUp() + self.Website = self.env['website'] + self.View = self.env['ir.ui.view'] + self.test_view = self.Website.viewref('test_website.test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_01_reset_specific_page_view(self): + self.test_page_view = self.Website.viewref('test_website.test_page_view') + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_page_view.with_context(website_id=1)) + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_page_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_02_reset_specific_view_controller(self): + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view.with_context(website_id=1)) + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_03_reset_specific_view_controller_t_called(self): + self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called') + + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view_to_be_t_called.with_context(website_id=1)) + break_view(self.test_view, to='<t t-call="test_website.test_view_to_be_t_called"/>') + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") + self.fix_it('/test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_04_reset_specific_view_controller_inherit(self): + self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken') + + # Activate and break the inherited view + self.test_view_child_broken.active = True + break_view(self.test_view_child_broken.with_context(website_id=1, load_all_views=True)) + + self.fix_it('/test_view') + + # This test work in real life, but not in test mode since we cannot rollback savepoint. + # @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + # def test_05_reset_specific_view_controller_broken_request(self): + # total_views = self.View.search_count([('type', '=', 'qweb')]) + # # Trigger COW then break the QWEB XML on it + # break_view(self.test_view.with_context(website_id=1), to='<t t-esc="request.env[\'website\'].browse(\'a\').name" />') + # self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (1)") + # self.fix_it('/test_view') + + # also mute ir.ui.view as `get_view_id()` will raise "Could not find view object with xml_id 'not.exist'"" + @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + def test_06_reset_specific_view_controller_inexisting_template(self): + total_views = self.View.search_count([('type', '=', 'qweb')]) + # Trigger COW then break the QWEB XML on it + break_view(self.test_view.with_context(website_id=1), to='<t t-call="not.exist"/>') + self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)") + self.fix_it('/test_view') + + @mute_logger('odoo.addons.website.models.ir_http') + def test_07_reset_page_view_complete_flow(self): + self.do_test('test_reset_page_view_complete_flow_part1') + self.fix_it('/test_page_view') + self.do_test('test_reset_page_view_complete_flow_part2') + self.fix_it('/test_page_view') diff --git a/addons/test_website/views/templates.xml b/addons/test_website/views/templates.xml new file mode 100644 index 00000000000..2ce171bac30 --- /dev/null +++ b/addons/test_website/views/templates.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_frontend" inherit_id="website.assets_frontend"> + <xpath expr="script[last()]" position="after"> + <script type="text/javascript" src="/test_website/static/src/js/reset_views.test.js"></script> + </xpath> + </template> +</odoo> diff --git a/addons/web/static/lib/moment/locale/es.js b/addons/web/static/lib/moment/locale/es.js index eb9e0809dfe..6d1f5907968 100644 --- a/addons/web/static/lib/moment/locale/es.js +++ b/addons/web/static/lib/moment/locale/es.js @@ -8,21 +8,30 @@ typeof define === 'function' && define.amd ? define(['../moment'], factory) : factory(global.moment) }(this, (function (moment) { 'use strict'; +var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'), + monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); - -var monthsShortDot = 'ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.'.split('_'); -var monthsShort = 'ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic'.split('_'); +var monthsParse = [/^ene/i, /^feb/i, /^mar/i, /^abr/i, /^may/i, /^jun/i, /^jul/i, /^ago/i, /^sep/i, /^oct/i, /^nov/i, /^dic/i]; +var monthsRegex = /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i; var es = moment.defineLocale('es', { months : 'enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre'.split('_'), monthsShort : function (m, format) { - if (/-MMM-/.test(format)) { + if (!m) { + return monthsShortDot; + } else if (/-MMM-/.test(format)) { return monthsShort[m.month()]; } else { return monthsShortDot[m.month()]; } }, - monthsParseExact : true, + monthsRegex : monthsRegex, + monthsShortRegex : monthsRegex, + monthsStrictRegex : /^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i, + monthsShortStrictRegex : /^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i, + monthsParse : monthsParse, + longMonthsParse : monthsParse, + shortMonthsParse : monthsParse, weekdays : 'domingo_lunes_martes_miércoles_jueves_viernes_sábado'.split('_'), weekdaysShort : 'dom._lun._mar._mié._jue._vie._sáb.'.split('_'), weekdaysMin : 'do_lu_ma_mi_ju_vi_sá'.split('_'), @@ -57,6 +66,7 @@ var es = moment.defineLocale('es', { future : 'en %s', past : 'hace %s', s : 'unos segundos', + ss : '%d segundos', m : 'un minuto', mm : '%d minutos', h : 'una hora', diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js index 34dd24b2be6..ef828b5dd5c 100644 --- a/addons/web/static/src/js/core/abstract_storage_service.js +++ b/addons/web/static/src/js/core/abstract_storage_service.js @@ -13,6 +13,17 @@ var AbstractStorageService = AbstractService.extend({ // this abstraction storage: null, + /** + * @override + */ + destroy: function () { + // storage can be permanent or transient, destroy transient ones + if ((this.storage || {}).destroy) { + this.storage.destroy(); + } + this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index 5d707d951bb..19916e79a4c 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -112,7 +112,17 @@ var utils = { var d2 = Math.pow(10, decimals); var val = _t('kMGTPE'); var symbol = ''; - for (var i = val.length - 1 ; i > 0 ; i--) { + var numberMagnitude = number.toExponential().split('e')[1]; + // the case numberMagnitude >= 21 corresponds to a number + // better expressed in the scientific format. + if (numberMagnitude >= 21) { + // we do not use number.toExponential(decimals) because we want to + // avoid the possible useless O decimals: 1e.+24 prefered to 1.0e+24 + number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2; + // formatterCallback seems useless here. + return number + 'e' + numberMagnitude; + } + for (var i = val.length; i > 0 ; i--) { var s = Math.pow(10, i * 3); if (s <= number / Math.pow(10, minDigits - 1)) { number = Math.round(number * d2 / s) / d2; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 4d266db409d..dc75ba8ea2a 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2911,6 +2911,7 @@ var AceEditor = DebouncedField.extend({ if (this.aceEditor) { this.aceEditor.destroy(); } + this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/control_panel/search/search_filters.js b/addons/web/static/src/js/views/control_panel/search/search_filters.js index 57aa34a84a7..80a9019c02d 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_filters.js +++ b/addons/web/static/src/js/views/control_panel/search/search_filters.js @@ -242,7 +242,7 @@ var DateTime = Field.extend({ _create_new_widget: function (name) { this[name] = new (this._get_widget_class())(this); return this[name].appendTo(this.$el).then((function () { - this[name].setValue(moment(new Date())); + this[name].setValue(moment()); }).bind(this)); }, _get_widget_class: function () { diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 09706695bbc..8cff755676c 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -456,6 +456,10 @@ return AbstractRenderer.extend({ // of the tiny margins. if (ticksLabels.length > 3) { $svgContainer.find('svg .nv-x g.nv-axisMaxMin-x > text').hide(); + } else { + // Since the graph is on full container have first and last label in the container + $svgContainer.find('svg .nv-x g.nv-axisMin-x > text').css({'text-anchor': 'start'}); + $svgContainer.find('svg .nv-x g.nv-axisMax-x > text').css({'text-anchor': 'end'}); } return chart; diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 4c37b823639..3f195c1ea24 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -237,7 +237,7 @@ var PivotModel = AbstractModel.extend({ [ this.data.timeRangeDescription.toString(), this.data.comparisonTimeRangeDescription.toString(), - 'Variation' + _t('Variation') ], makeMeasure ) @@ -443,8 +443,10 @@ var PivotModel = AbstractModel.extend({ self._updateMainGroupBys(old_row, self.data.main_row); } - self._updateTree(old_col.root, self.data.main_col.root); - self._updateMainGroupBys(old_col, self.data.main_col); + if (!('pivot_column_groupby' in (params.context || {}))) { + self._updateTree(old_col.root, self.data.main_col.root); + self._updateMainGroupBys(old_col, self.data.main_col); + } }); }, /** diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index 787e28e38a9..8352759c695 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -6,6 +6,7 @@ var core = require('web.core'); var field_utils = require('web.field_utils'); var QWeb = core.qweb; +var _t = core._t; var PivotRenderer = AbstractRenderer.extend({ tagName: 'table', @@ -171,7 +172,7 @@ var PivotRenderer = AbstractRenderer.extend({ $thead.append($row); } if (this.compare) { - var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, 'Variation']; + var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, _t('Variation')]; var dataTypes = ['data', 'comparisonData', 'variation']; $row = $('<tr>'); for (i = 0; i < 3 * nbrCols; i++) { diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index 5642e656dea..0645e00544e 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -744,6 +744,10 @@ .o_field_widget { width: 50%; flex: 0 0 auto; + + &.o_field_many2manytags > .o_field_widget { + flex: 1 0 50px; + } } button.btn-link:first-child { padding-left: 0; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 5e2f27ccc66..e11184ebb1e 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -348,7 +348,7 @@ <t t-raw="item.label"/> </a> </t> - <div class="dropdown-menu" role="menu"> + <div class="dropdown-menu o_dropdown_menu" role="menu"> <t t-foreach="widget.items[section.name]" t-as="item"> <t t-if="section.name == 'files'"> <t t-set="item.title"> diff --git a/addons/web/static/tests/chrome/user_menu_tests.js b/addons/web/static/tests/chrome/user_menu_tests.js index 600ac125dd4..0151afdf6d6 100644 --- a/addons/web/static/tests/chrome/user_menu_tests.js +++ b/addons/web/static/tests/chrome/user_menu_tests.js @@ -25,6 +25,7 @@ QUnit.module('chrome', {}, function () { "should have a 'Shortcuts' item"); userMenu.destroy(); + parent.destroy(); }); }); diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js index c7158083dcb..f81dea7ce4e 100644 --- a/addons/web/static/tests/core/util_tests.js +++ b/addons/web/static/tests/core/util_tests.js @@ -55,6 +55,26 @@ QUnit.module('core', {}, function () { assert.strictEqual(is_bin_size('64.2 Кб'), true); }); + QUnit.test('human_number', function (assert) { + assert.expect(13); + + var human_number = utils.human_number; + + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1020000, 2, 2), '1020k'); + assert.strictEqual(human_number(10200000, 2, 2), '10.2M'); + assert.strictEqual(human_number(1020, 2, 1), '1.02k'); + assert.strictEqual(human_number(1002, 2, 1), '1k'); + assert.strictEqual(human_number(101, 2, 1), '101'); + assert.strictEqual(human_number(64.2, 2, 1), '64'); + assert.strictEqual(human_number(1e+18), '1E'); + assert.strictEqual(human_number(1e+21, 2, 1), '1e+21'); + assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22'); + assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22'); + assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43'); + assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43'); + }); + }); -});
\ No newline at end of file +}); diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js index 834033a1769..0eb4192c4a1 100644 --- a/addons/web/static/tests/core/widget_tests.js +++ b/addons/web/static/tests/core/widget_tests.js @@ -24,6 +24,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy('exec'); fn(); assert.ok(w.executed, 'should execute the named method in the right context'); + w.destroy(); }); QUnit.test('proxy (String)(*args)', function (assert) { @@ -39,6 +40,7 @@ QUnit.module('core', {}, function () { fn(42); assert.ok(w.executed, "should execute the named method in the right context"); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); QUnit.test('proxy (String), include', function (assert) { @@ -59,6 +61,7 @@ QUnit.module('core', {}, function () { fn(); assert.strictEqual(w.executed, 2, "should be lazily resolved"); + w.destroy(); }); QUnit.test('proxy (Function)', function (assert) { @@ -69,6 +72,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function () { this.executed = true; }); fn(); assert.ok(w.executed, "should set the function's context (like Function#bind)"); + w.destroy(); }); QUnit.test('proxy (Function)(*args)', function (assert) { @@ -79,6 +83,7 @@ QUnit.module('core', {}, function () { var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments"); + w.destroy(); }); @@ -99,6 +104,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element"); assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute"); assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content")); + widget.destroy(); }); QUnit.test('no template, custom tag', function (assert) { @@ -111,6 +117,7 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual(widget.el.nodeName, 'UL', "should have generated the custom element tag"); + widget.destroy(); }); QUnit.test('no template, @id', function (assert) { @@ -124,6 +131,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.attributes.length, 1, "should have one attribute"); assert.hasAttrValue(widget.$el, 'id', 'foo', "should have generated the id attribute"); assert.strictEqual(widget.el.id, 'foo', "should also be available via property"); + widget.destroy(); }); QUnit.test('no template, @className', function (assert) { @@ -136,6 +144,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.className, 'oe_some_class', "should have the right property"); assert.hasAttrValue(widget.$el, 'class', 'oe_some_class', "should have the right attribute"); + widget.destroy(); }); QUnit.test('no template, bunch of attributes', function (assert) { @@ -166,6 +175,7 @@ QUnit.module('core', {}, function () { assert.hasAttrValue(widget.$el, 'clark', 'gable'); assert.hasAttrValue(widget.$el, 'spoiler', 'snape kills dumbledore'); + widget.destroy(); }); QUnit.test('template', function (assert) { @@ -193,6 +203,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'OL'); assert.strictEqual(widget.$el.children().length, 5); assert.strictEqual(widget.el.textContent, '01234'); + widget.destroy(); }); QUnit.test('repeated', function (assert) { @@ -219,6 +230,8 @@ QUnit.module('core', {}, function () { widget.renderElement(); assert.strictEqual($fix.find('p').text(), '36', "DOM fixture should use new value"); assert.strictEqual(widget.$el.text(), '36', "should set new value"); + }).always(function () { + widget.destroy(); }); }); @@ -257,6 +270,7 @@ QUnit.module('core', {}, function () { assert.ok(widget.$('li:eq(3)').is(widget.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); + widget.destroy(); }); @@ -285,6 +299,7 @@ QUnit.module('core', {}, function () { for(var i=0; i<3; ++i) { assert.ok(a[i], "should pass test " + i); } + widget.destroy(); }); QUnit.test('undelegate', function (assert) { @@ -310,6 +325,7 @@ QUnit.module('core', {}, function () { widget.$('li').click(); assert.ok(!clicked, "undelegate should unbind events delegated"); assert.ok(newclicked, "undelegate should only unbind events it created"); + widget.destroy(); }); QUnit.module('Widget, and async stuff'); @@ -321,7 +337,8 @@ QUnit.module('core', {}, function () { return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen()); }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead)", function (assert) { @@ -355,7 +372,8 @@ QUnit.module('core', {}, function () { var widget = new (Widget.extend({})); return concurrency.asyncWhen(widget.start()) .then(function () { return widget.alive(concurrency.asyncWhen(), true) }) - .then(function () { assert.ok(true); }); + .then(function () { assert.ok(true); }) + .always(function () { widget.destroy(); }); }); QUnit.test("alive(dead, true)", function (assert) { @@ -445,4 +463,4 @@ QUnit.module('core', {}, function () { }); -});
\ No newline at end of file +}); diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 3eaaae0a46f..ad7878001f3 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -198,6 +198,11 @@ function addMockEnvironment(widget, params) { // widget is destroyed, at the end of each test to avoid collisions core.bus.trigger('clear_cache'); + _(services).chain() + .compact() // services can be defined but null (e.g. ajax) + .reject(function (s) { return s.isDestroyed(); }) + .invoke('destroy'); + DebouncedField.prototype.DEBOUNCE = initialDebounceValue; dom.DEBOUNCE = initialDOMDebounceValue; if (params.debounce === false) { diff --git a/addons/web/static/tests/views/abstract_controller_tests.js b/addons/web/static/tests/views/abstract_controller_tests.js index cf5810219c7..66827540fc2 100644 --- a/addons/web/static/tests/views/abstract_controller_tests.js +++ b/addons/web/static/tests/views/abstract_controller_tests.js @@ -67,6 +67,8 @@ odoo.define("base.abstract_controller_tests", function(require) { testUtils.dom.click(view.$(".simple")); testUtils.dom.click(view.$(".with-child span")); assert.verifySteps(["a1", "a2"]); + + view.destroy(); } ); } diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 7af2892ac33..9abaac5ad1e 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -1007,6 +1007,95 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('Unload Filter, reset display, load another filter', function (assert) { + assert.expect(18); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + // Equivalent to unload the filter + var reloadParams = { + context: {}, + }; + pivot.reload(reloadParams); + // collapse all headers + pivot.$('.o_pivot_header_cell_opened').click(); + pivot.$('.o_pivot_header_cell_opened').click(); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed').length, 1, + 'The column should not be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 0, + 'There should not be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 0, + 'There should not be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed').length, 1, + 'The row should not be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 0, + 'There should not be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 0, + 'There should not be a row "xpad"'); + + // Equivalent to load another filter + reloadParams = { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }; + pivot.reload(reloadParams); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); @@ -1405,7 +1494,7 @@ QUnit.module('Views', { }); QUnit.test('rendering of pivot view with comparison', function (assert) { - assert.expect(91); + assert.expect(92); this.data.partner.records[0].date = '2016-12-15'; this.data.partner.records[1].date = '2016-12-17'; @@ -1442,6 +1531,17 @@ QUnit.module('Views', { '</pivot>', 'partner,false,search': '<search></search>', }, + intercepts: { + create_filter: function (ev) { + var data = ev.data; + assert.deepEqual(data.filter.context.timeRangeMenuData, { + timeRange: ["&",["date",">=","2016-12-01"],["date","<","2017-01-01"]], + timeRangeDescription: 'This Month', + comparisonTimeRange: ["&",["date",">=","2016-11-01"],["date","<","2016-12-01"]], + comparisonTimeRangeDescription: 'Previous Period', + }); + } + } }); actionManager.doAction({ @@ -1512,6 +1612,12 @@ QUnit.module('Views', { ]; checkCellValues(results); + $('.o_search_options button:contains("Favorites")').click(); + var $favorites = $('.dropdown-menu.o_favorites_menu'); + $favorites.find('a.o_save_search').click(); + $favorites.find('.o_input').val('Fav').trigger('input'); + $favorites.find('button').click(); + unpatchDate(); actionManager.destroy(); }); diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index e281d9f9ecc..41de79b3c24 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -159,6 +159,10 @@ class IrUiView(models.Model): out.tail = el.tail return out + @api.model + def _set_noupdate(self): + self.sudo().mapped('model_data_id').write({'noupdate': True}) + @api.multi def save(self, value, xpath=None): """ Update a view section. The view section may embed fields to write @@ -195,16 +199,12 @@ class IrUiView(models.Model): new_arch = self.replace_arch_section(xpath, arch_section) old_arch = etree.fromstring(self.arch.encode('utf-8')) if not self._are_archs_equal(old_arch, new_arch): - self.sudo().model_data_id.write({'noupdate': True}) # TODO check if we remove this + self._set_noupdate() self.write({'arch': self._pretty_arch(new_arch)}) @api.model def _view_get_inherited_children(self, view, options): - extensions = view.inherit_children_ids - if not options: - # only active children - extensions = extensions.filtered(lambda view: view.active) - return extensions + return view.inherit_children_ids @api.model def _view_obj(self, view_id): @@ -252,6 +252,9 @@ class IrUiView(models.Model): views_to_return += self._views_get(called_view, options=options, bundles=bundles) extensions = self._view_get_inherited_children(view, options) + if not options: + # only active children + extensions = extensions.filtered(lambda view: view.active) # Keep options in a deterministic order regardless of their applicability for extension in extensions.sorted(key=lambda v: v.id): diff --git a/addons/web_editor/static/src/scss/web_editor.ui.scss b/addons/web_editor/static/src/scss/web_editor.ui.scss index 922a76fdb6f..c98d0245f7f 100644 --- a/addons/web_editor/static/src/scss/web_editor.ui.scss +++ b/addons/web_editor/static/src/scss/web_editor.ui.scss @@ -1086,7 +1086,7 @@ body.editor_enable { &:checked + span { box-shadow: none; - background-image: linear-gradient(0deg, $o-we-color-success, darken($o-we-color-success, 10%)); + background: $o-we-color-success; &:after { margin-left: ($o-we-switch-size*1.8 - $o-we-switch-size) + 0.1; @@ -1097,7 +1097,7 @@ body.editor_enable { > input { &:not(:checked) + span { box-shadow: none; - background-image: linear-gradient(0deg, lighten(theme-color('danger'), 5%), darken(theme-color('danger'), 5%)); + background: $o-we-color-danger; } } } diff --git a/addons/web_editor/static/src/scss/web_editor.variables.scss b/addons/web_editor/static/src/scss/web_editor.variables.scss index e556d6e5e4e..679b82c10fc 100644 --- a/addons/web_editor/static/src/scss/web_editor.variables.scss +++ b/addons/web_editor/static/src/scss/web_editor.variables.scss @@ -111,10 +111,17 @@ $o-we-switch-inactive-color: $o-we-color-paper !default; color: #555; background-color: #fff; border-radius: 0; + &.is-invalid { border-color: $danger; } } + .input-group .form-control { + height: auto; + } + .input-group-text { + background-color: #e9ecef; + } .was-validated { .form-control:invalid { @@ -137,10 +144,14 @@ $o-we-switch-inactive-color: $o-we-color-paper !default; border-radius: 0; background-color: $o-we-color-paper; + .modal-header { + border-bottom-color: #e9ecef; + } .modal-body { background-color: white; } .modal-footer { + border-top-color: #e9ecef; text-align: left; } } @@ -148,23 +159,23 @@ $o-we-switch-inactive-color: $o-we-color-paper !default; @mixin o-w-preserve-tabs { .nav-tabs { - border-bottom: 1px solid gray('200'); + border-bottom: 1px solid #e9ecef; > li { > a { line-height: 1.5; color: $o-we-color-light; + &:hover { - border-color: gray('200'); + border-color: #dee2e6; } - } - &.active > a { - &, &:hover, &:focus { - color: $o-we-color-normal; - background-color: $o-we-color-paper; - border-color: gray('200'); - border-bottom-color: transparent; + &.active { + &, &:hover, &:focus { + color: $o-we-color-normal; + background-color: $o-we-color-paper; + border-color: #dee2e6 #dee2e6 #FFFFFF; + } } } } @@ -176,7 +187,7 @@ $o-we-switch-inactive-color: $o-we-color-paper !default; border-radius: 0; font-weight: normal; text-transform: none; - @include button-size(.375rem, .75rem, 1rem, 1.5, 0); + @include button-size(.375rem, .75rem, 0.875rem, 1.5, 0); &.btn-primary { @include button-variant($o-brand-primary, $o-brand-primary); @@ -201,13 +212,6 @@ $o-we-switch-inactive-color: $o-we-color-paper !default; &.btn-danger { @include button-variant($o-we-color-danger, $o-we-color-danger); } - - &.btn-lg { - @include button-size(.5rem, 1rem, 1.25rem, 1.5, 0); - } - &.btn-sm { - @include button-size(.25rem, .5rem, .875rem, 1.5, 0); - } } } diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 5fcfac2f4aa..2760ed1b0ab 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -417,7 +417,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_size_x">Background width</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -428,7 +428,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_size_y">Background height</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -443,7 +443,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_pos_x">Horizontal</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control " id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> @@ -454,7 +454,7 @@ <label class="col-md-3 col-form-label" for="o_bg_img_opt_custom_pos_y">Vertical</label> <div class="col-md-5"> <div class="input-group"> - <input type="number" min="0" max="100" class="form-control form-control-sm" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> + <input type="number" min="0" max="100" class="form-control" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> <div class="input-group-append"> <div class="input-group-text">%</div> </div> diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index de83a7cb8cf..7773dcdc489 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -286,24 +286,36 @@ class Website(Home): views = views.sorted(key=lambda v: (v.inherit_id.id, v.name)) return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) - @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True) + @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True, csrf=False) def reset_template(self, templates, redirect='/', **kwargs): + """ This method will try to reset a list of broken views ids. + It will read the original `arch` from the view's XML file (arch_fs). + Views without an `arch_fs` can't be reset, except views created when + dropping a snippet in specific oe_structure that create an inherited + view doing an xpath. + Note: The `arch_fs` field is automatically erased when there is a + write on the `arch` field. + + This method is typically useful to reset specific views. In that case we + read the XML file from the generic view. + """ templates = request.httprequest.form.getlist('templates') - modules_to_update = [] for temp_id in templates: view = request.env['ir.ui.view'].browse(int(temp_id)) - if view.page: + if 'oe_structure' in view.key: + # Particular xpathing view created in edit mode + view.unlink() continue - view.model_data_id.write({ - 'noupdate': False - }) - if view.model_data_id.module not in modules_to_update: - modules_to_update.append(view.model_data_id.module) + xml_view = view._get_original_view() # view might already be the xml_view + if xml_view.arch_fs: + view_file_arch = xml_view.with_context(read_arch_from_file=True).arch + # Deactivate COW to not fix a generic view by creating a specific + view.with_context(website_id=None).arch_db = view_file_arch + if view == xml_view: + view.model_data_id.write({ + 'noupdate': False + }) - if modules_to_update: - modules = request.env['ir.module.module'].sudo().search([('name', 'in', modules_to_update)]) - if modules: - modules.button_immediate_upgrade() return request.redirect(redirect) @http.route(['/website/publish'], type='json', auth="public", website=True) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index bfaa9c6fce6..42a3580a18f 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +from lxml import etree import traceback import os import unittest @@ -12,14 +13,14 @@ import werkzeug.routing import werkzeug.utils import odoo -from odoo import api, models +from odoo import api, models, registry from odoo import SUPERUSER_ID from odoo.http import request from odoo.tools import config -from odoo.exceptions import QWebException from odoo.tools.safe_eval import safe_eval from odoo.osv.expression import FALSE_DOMAIN, OR +from odoo.addons.base.models.qweb import QWebException from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype from odoo.addons.portal.controllers.portal import _build_url_w_params @@ -185,7 +186,7 @@ class Http(models.AbstractModel): # if parent excplicitely returns a plain response, then we don't touch it return response except Exception as e: - if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')): + if 'werkzeug' in config['dev_mode']: raise e exception = e @@ -207,19 +208,9 @@ class Http(models.AbstractModel): if isinstance(exception, QWebException): values.update(qweb_exception=exception) - if isinstance(exception.qweb.get('cause'), odoo.exceptions.AccessError): + if type(exception.error) == odoo.exceptions.AccessError: code = 403 - if code == 500: - logger.error("500 Internal Server Error:\n\n%s", values['traceback']) - if 'qweb_exception' in values: - view = request.env["ir.ui.view"] - views = view._views_get(exception.qweb['template']) - to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and view.arch_fs) - values['views'] = to_reset - elif code == 403: - logger.warn("403 Forbidden:\n\n%s", values['traceback']) - values.update( status_message=werkzeug.http.HTTP_STATUS_CODES[code], status_code=code, @@ -233,10 +224,51 @@ class Http(models.AbstractModel): if not request.uid: cls._auth_method_public() - try: - html = request.env['ir.ui.view'].render_template('website.%s' % view_id, values) - except Exception: - html = request.env['ir.ui.view'].render_template('website.http_error', values) + with registry(request.env.cr.dbname).cursor() as cr: + env = api.Environment(cr, request.uid, request.env.context) + if code == 500: + logger.error("500 Internal Server Error:\n\n%s", values['traceback']) + View = env["ir.ui.view"] + if 'qweb_exception' in values: + if 'load could not load template' in exception.args: + # When t-calling an inexisting template, we don't have reference to + # the view that did the t-call. We need to find it. + values['views'] = View.search([ + ('type', '=', 'qweb'), + '|', + ('arch_db', 'ilike', 't-call="%s"' % exception.name), + ('arch_db', 'ilike', "t-call='%s'" % exception.name) + ], order='write_date desc', limit=1) + else: + try: + # exception.name might be int, string + exception_template = int(exception.name) + except: + exception_template = exception.name + view = View._view_obj(exception_template) + et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch']) + node = et.find(exception.path.replace('/templates/t/', './')) + line = node is not None and etree.tostring(node, encoding='unicode') + # line = exception.html # FALSE -> contains branding <div t-att-data="request.browse('ok')"/> + if line: + # If QWebException occurs in a child view, the parent view is raised + values['editable'] = request.uid and request.website.is_publisher() + values['views'] = View._views_get(exception_template).filtered( + lambda v: line in v.arch + ) + else: + values['views'] = view + # Keep only views that we can reset + values['views'] = values['views'].filtered( + lambda view: view._get_original_view().arch_fs or 'oe_structure' in view.key + ) + elif code == 403: + logger.warn("403 Forbidden:\n\n%s", values['traceback']) + try: + html = env['ir.ui.view'].render_template('website.%s' % view_id, values) + except Exception: + html = env['ir.ui.view'].render_template('website.http_error', values) + return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8') def binary_content(self, xmlid=None, model='ir.attachment', id=None, field='datas', diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index dc3ebb286c3..27fe7a66437 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -278,6 +278,16 @@ class View(models.Model): return super(View, self).get_view_id(xml_id) @api.multi + def _get_original_view(self): + """Given a view, retrieve the original view it was COW'd from. + The given view might already be the original one. In that case it will + (and should) return itself. + """ + self.ensure_one() + domain = [('key', '=', self.key), ('model_data_id', '!=', None)] + return self.search(domain, limit=1) # Useless limit has multiple xmlid should not be possible + + @api.multi def render(self, values=None, engine='ir.qweb', minimal_qcontext=False): """ Render the template. If website is enabled on request, then extend rendering context with website values. """ new_context = dict(self._context) @@ -371,6 +381,15 @@ class View(models.Model): res['website_id'] = self.env['website'].get_current_website().id return res + @api.model + def _set_noupdate(self): + '''If website is installed, any call to `save` from the frontend will + actually write on the specific view (or create it if not exist yet). + In that case, we don't want to flag the generic view as noupdate. + ''' + if not self._context.get('website_id'): + super(View, self)._set_noupdate() + @api.multi def save(self, value, xpath=None): self.ensure_one() diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py index 45d65919cd9..5cbba699981 100644 --- a/addons/website/models/res_partner.py +++ b/addons/website/models/res_partner.py @@ -52,3 +52,10 @@ class Partner(models.Model): # onchange uses the cache to retrieve value, we need to copy computed_value into the initial env for record, record2 in zip(self, self2): record.display_name = record2.display_name + + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(Partner, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 96648264880..122ed2c4dfd 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -455,7 +455,8 @@ class Website(models.Model): if website_id: return self.browse(website_id) - domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None + # The format of `httprequest.host` is `domain:port` + domain_name = request and request.httprequest.host or '' country = request.session.geoip.get('country_code') if request and request.session.geoip else False country_id = False @@ -466,9 +467,64 @@ class Website(models.Model): return self.browse(website_id) @tools.cache('domain_name', 'country_id', 'fallback') + @api.model def _get_current_website_id(self, domain_name, country_id, fallback=True): - # sort on country_group_ids so that we fall back on a generic website (empty country_group_ids) - websites = self.search([('domain', '=', domain_name)]).sorted('country_group_ids') + """Get the current website id. + + First find all the websites for which the configured `domain` (after + ignoring a potential scheme) is equal to the given + `domain_name`. If there is only one result, return it immediately. + + If there are no website found for the given `domain_name`, either + fallback to the first found website (no matter its `domain`) or return + False depending on the `fallback` parameter. + + If there are multiple websites for the same `domain_name`, we need to + filter them out by country. We return the first found website matching + the given `country_id`. If no found website matching `domain_name` + corresponds to the given `country_id`, the first found website for + `domain_name` will be returned (no matter its country). + + :param domain_name: the domain for which we want the website. + In regard to the `url_parse` method, only the `netloc` part should + be given here, no `scheme`. + :type domain_name: string + + :param country_id: id of the country for which we want the website + :type country_id: int + + :param fallback: if True and no website is found for the specificed + `domain_name`, return the first website (without filtering them) + :type fallback: bool + + :return: id of the found website, or False if no website is found and + `fallback` is False + :rtype: int or False + + :raises: if `fallback` is True but no website at all is found + """ + def _remove_port(domain_name): + return (domain_name or '').split(':')[0] + + def _filter_domain(website, domain_name, ignore_port=False): + """Ignore `scheme` from the `domain`, just match the `netloc` which + is host:port in the version of `url_parse` we use.""" + # Here we add http:// to the domain if it's not set because + # `url_parse` expects it to be set to correctly return the `netloc`. + website_domain = urls.url_parse(website._get_http_domain()).netloc + if ignore_port: + website_domain = _remove_port(website_domain) + domain_name = _remove_port(domain_name) + return website_domain.lower() == (domain_name or '').lower() + + # Sort on country_group_ids so that we fall back on a generic website: + # websites with empty country_group_ids will be first. + found_websites = self.search([('domain', 'ilike', _remove_port(domain_name))]).sorted('country_group_ids') + # Filter for the exact domain (to filter out potential subdomains) due + # to the use of ilike. + websites = found_websites.filtered(lambda w: _filter_domain(w, domain_name)) + # If there is no domain matching for the given port, ignore the port. + websites = websites or found_websites.filtered(lambda w: _filter_domain(w, domain_name, ignore_port=True)) if not websites: if not fallback: @@ -730,6 +786,19 @@ class Website(models.Model): 'target': 'self', } + @api.multi + def _get_http_domain(self): + """Get the domain of the current website, prefixed by http if no + scheme is specified. + + Empty string if no domain is specified on the website. + """ + self.ensure_one() + if not self.domain: + return '' + res = urls.url_parse(self.domain) + return 'http://' + self.domain if not res.scheme else self.domain + class SeoMetadata(models.AbstractModel): diff --git a/addons/website/static/src/js/tours/website_tour_reset_password.js b/addons/website/static/src/js/tours/website_tour_reset_password.js new file mode 100644 index 00000000000..2b304bb0103 --- /dev/null +++ b/addons/website/static/src/js/tours/website_tour_reset_password.js @@ -0,0 +1,181 @@ +odoo.define('website.tour_reset_password', function (require) { +'use strict'; + +var base = require('web_editor.base'); +var localStorage = require('web.local_storage'); +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); + +var currentDomain = window.location.protocol + '//' + window.location.hostname; +var emailsUrl = '/web#action=mail.action_view_mail_mail&view_type=list'; +var usersUrl = '/web#action=base.action_res_users&view_type=list'; +var resetLinkKey = 'website.tour_reset_password.resetLink'; + +tour.register('website_reset_password', { + test: true, + url: '/web', + wait_for: base.ready(), +}, +[ + { + content: "Change the domain of the websites and go to users page", + trigger: '.oe_topbar_name:contains("Admin")', + run: function () { + // We could do it with the UI but this is not the goal of this test, + // so we just make an RPC to be faster. + + // We change the domain of the website to test that the email that + // will be sent uses the correct domain for its links. + var def1 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1], { + 'domain': "my-test-domain.com", + }], + }); + // We need to change the domain of all the websites otherwise the + // website selector will return the website 2 since the domain we + // set on website 1 doesn't actually match our test server. + var def2 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[2], { + 'domain': "https://domain-not-used.fr", + }], + }); + // Set a website on "Admin" partner to test the URL of the website. + var def3 = rpc.query({ + model: 'res.partner', + method: 'name_search', + kwargs: {'name': 'Mitchell Admin'}, + }).then(function (res) { + return rpc.query({ + 'model': 'res.partner', + 'method': 'write', + 'args': [[res[0][0]], { + 'website_id': 1, + }], + }); + }); + + return $.when(def1, def2, def3).then(function () { + window.location.href = usersUrl; + }); + }, + }, + { + content: "click on Admin", + trigger: '.o_data_cell:contains("Admin")', + }, + { + content: "click on reset password", + trigger: '.btn[name="action_reset_password"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: 'a[name="signup_url"][href^="http://my-test-domain.com"]', + run: function () { + window.location.href = emailsUrl; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("Password reset"):eq(0)', + }, + { + content: "check page has an iframe", + trigger: 'iframe body', + run: function () { + var content = $('iframe').contents()[0]; + console.log(content && new XMLSerializer().serializeToString(content)); + }, + }, + { + content: "check iframe has the button", + trigger: 'iframe a:contains("Change password")', + run: function () {}, + }, + { + content: "check the URL is correct too", + trigger: 'iframe a:contains("Change password")[href^="http://my-test-domain.com"]', + run: function () { + // reset the domain of the websites, go to users page + return rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1, 2], { + 'domain': "", + }], + }).then(function () { + window.location.href = usersUrl; + }); + }, + }, + { + content: "click on Admin", + trigger: '.o_data_cell:contains("Admin")', + }, + { + content: "click on reset password", + trigger: '.btn[name="action_reset_password"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: 'a[name="signup_url"][href^="' + currentDomain + '"]', + run: function () { + window.location.href = emailsUrl; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("Password reset"):eq(0)', + }, + { + content: "check the link has the current host, save the link, logout", + trigger: 'iframe a:contains("Change password")[href^="' + currentDomain + '"]', + run: function () { + var link = $('iframe').contents().find('a:contains("Change password")').attr('href'); + localStorage.setItem(resetLinkKey, link); + window.location.href = "/web/session/logout?redirect=/"; + }, + }, + { + content: "go to the reset link", + trigger: 'a[href="/web/login"]', + run: function () { + window.location.href = localStorage.getItem(resetLinkKey); + }, + }, + { + content: "fill new password and submit", + trigger: '.oe_reset_password_form', + run: function () { + var $form = $('.oe_reset_password_form'); + $form.find('input[name="password"]').val('adminadmin'); + // password must be at least 8 if the module `auth_password_policy` + // is installed which is the case on runbot + $form.find('input[name="confirm_password"]').val('adminadmin'); + $form.find('button[type="submit"]').click(); + }, + }, + { + content: "check logged in, and reset admin website", + trigger: '.oe_topbar_name:contains("Admin")', + run: function () { + return rpc.query({ + model: 'res.partner', + method: 'name_search', + kwargs: {'name': 'Admin'}, + }).then(function (res) { + return rpc.query({ + 'model': 'res.partner', + 'method': 'write', + 'args': [[res[0][0]], { + 'website_id': false, + }], + }); + }); + }, + }, +]); +}); diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss index d9554a20a1c..fe218765174 100644 --- a/addons/website/static/src/scss/website.ui.scss +++ b/addons/website/static/src/scss/website.ui.scss @@ -134,10 +134,14 @@ body .modal { cursor: pointer; font-family: $o-we-font-family; + &, .close { + color: white; + } + h4 { - color: lightgray; - font-family:inherit; + font-family: inherit; font-weight: normal; + color: inherit; .fa { margin-left: $grid-gutter-width/2; diff --git a/addons/website/static/src/xml/website.pageProperties.xml b/addons/website/static/src/xml/website.pageProperties.xml index 8ad1d995ce5..348da5e9c17 100644 --- a/addons/website/static/src/xml/website.pageProperties.xml +++ b/addons/website/static/src/xml/website.pageProperties.xml @@ -49,7 +49,7 @@ <div class="form-group row"> <label class="col-form-label col-md-3" for="page_name">Page Name</label> <div class="col-md-9"> - <input type="text" class="form-control form-control-sm" id="page_name" t-att-value="widget.page.name" /> + <input type="text" class="form-control" id="page_name" t-att-value="widget.page.name" /> </div> </div> <div class="form-group warn_about_call"></div> @@ -60,7 +60,7 @@ <div class="input-group-prepend"> <span class="input-group-text" t-att-title="widget.serverUrl"><small><t t-esc="widget.serverUrlTrunc"/></small></span> </div> - <input type="text" class="form-control form-control-sm" id="page_url" t-att-value="widget.page.url" /> + <input type="text" class="form-control" id="page_url" t-att-value="widget.page.url" /> </div> </div> </div> diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 1c57eeab793..85ecad1cba8 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -2,9 +2,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_converter +from . import test_get_current_website from . import test_qweb from . import test_crawl from . import test_ui from . import test_views from . import test_menu from . import test_page +from . import test_website_reset_password diff --git a/addons/website/tests/test_get_current_website.py b/addons/website/tests/test_get_current_website.py new file mode 100644 index 00000000000..a71126cb484 --- /dev/null +++ b/addons/website/tests/test_get_current_website.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged('post_install', '-at_install') +class TestGetCurrentWebsite(TransactionCase): + + def test_01_get_current_website_id(self): + """Make sure `_get_current_website_id works`.""" + + Website = self.env['website'] + + # clean initial state + website1 = Website.browse(1) + website1.domain = '' + website1.country_group_ids = False + + website2 = Website.browse(2) + website2.domain = '' + website2.country_group_ids = False + + country1 = self.env['res.country'].create({'name': "My Country 1"}) + country2 = self.env['res.country'].create({'name': "My Country 2"}) + country3 = self.env['res.country'].create({'name': "My Country 3"}) + country4 = self.env['res.country'].create({'name': "My Country 4"}) + country5 = self.env['res.country'].create({'name': "My Country 5"}) + + country_group_1_2 = self.env['res.country.group'].create({ + 'name': "My Country Group 1-2", + 'country_ids': [(6, 0, (country1 + country2 + country5).ids)], + }) + country_group_3 = self.env['res.country.group'].create({ + 'name': "My Country Group 3", + 'country_ids': [(6, 0, (country3 + country5).ids)], + }) + + # CASE: no domain, no country: get first + self.assertEqual(Website._get_current_website_id('', False), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website1.id) + + # CASE: no domain, given country: get by country + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('', country1.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country2.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website2.id) + + # CASE: no domain, wrong country: get first + self.assertEqual(Website._get_current_website_id('', country4.id), website1.id) + + # CASE: no domain, multiple country: get first + self.assertEqual(Website._get_current_website_id('', country5.id), website1.id) + + # setup domain + website1.domain = 'my-site-1.fr' + website2.domain = 'https://my2ndsite.com:80' + + website1.country_group_ids = False + website2.country_group_ids = False + + # CASE: domain set: get matching domain + self.assertEqual(Website._get_current_website_id('my-site-1.fr', False), website1.id) + + # CASE: domain set: get matching domain (scheme and port supported) + self.assertEqual(Website._get_current_website_id('my-site-1.fr:8069', False), website1.id) + + self.assertEqual(Website._get_current_website_id('my2ndsite.com:80', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com:8069', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', False), website2.id) + + # CASE: domain set, wrong domain: get first + self.assertEqual(Website._get_current_website_id('test.com', False), website1.id) + + # CASE: subdomain: not supported + self.assertEqual(Website._get_current_website_id('www.my2ndsite.com', False), website1.id) + + # CASE: domain set, given country: get by domain in priority + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country1.id), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country2.id), website2.id) + self.assertEqual(Website._get_current_website_id('my-site-1.fr', country3.id), website1.id) + + # CASE: domain set, wrong country: get first for domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country4.id), website2.id) + + # CASE: domain set, multiple country: get first for domain + website1.domain = website2.domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country5.id), website1.id) + + # CASE: overlapping domain: get exact match + website1.domain = 'site-1.com' + website2.domain = 'even-better-site-1.com' + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-better-site-1.com', False), website2.id) + + # CASE: case insensitive + website1.domain = 'Site-1.com' + website2.domain = 'Even-Better-site-1.com' + self.assertEqual(Website._get_current_website_id('sitE-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-beTTer-site-1.com', False), website2.id) + + # CASE: same domain, different port + website1.domain = 'site-1.com:80' + website2.domain = 'site-1.com:81' + self.assertEqual(Website._get_current_website_id('site-1.com:80', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com:81', False), website2.id) + self.assertEqual(Website._get_current_website_id('site-1.com:82', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 9ece6cf4c3d..0416bc72bf7 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -159,6 +159,12 @@ class TestViewSaving(common.TransactionCase): h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund", type='char'))), ) ), encoding='unicode') + + self.view_id.with_context(website_id=1).save(value=replacement, xpath='/div/div[2]') + self.assertFalse(imd.noupdate, "view's xml_id shouldn't be set to 'noupdate' in a website context as `save` method will COW") + # remove newly created COW view so next `save()`` wont be redirected to COW view + self.env['website'].with_context(website_id=1).viewref(self.view_id.key).unlink() + self.view_id.save(value=replacement, xpath='/div/div[2]') # the xml_id of the view should be flagged as 'noupdate' @@ -622,6 +628,76 @@ class TestCowViewSaving(common.TransactionCase): views = View.with_context(website_id=1).get_related_views('B') self.assertEqual(views.mapped('key'), ['B', 'I', 'II'], "Should only return the specific tree") + def test_get_related_views_tree_recursive_t_call_and_inherit_inactive(self): + """ If a view A was doing a t-call on a view B and view B had view C as child. + And view A had view D as child. + And view D also t-call view B (that as mentionned above has view C as child). + And view D was inactive (`d` in bellow schema). + + Then COWing C to set it as inactive would make `get_related_views()` on A to return + both generic active C and COW inactive C. + (Typically the case for Customize show on /shop for Wishlist, compare..) + See commit message for detailed explanation. + """ + # A -> B + # | ^ \ + # | | C + # d ___| + + View = self.env['ir.ui.view'] + Website = self.env['website'] + + products = View.create({ + 'name': 'Products', + 'type': 'qweb', + 'key': '_website_sale.products', + 'arch': ''' + <div id="products_grid"> + <t t-call="_website_sale.products_item"/> + </div> + ''', + }) + + products_item = View.create({ + 'name': 'Products item', + 'type': 'qweb', + 'key': '_website_sale.products_item', + 'arch': ''' + <div class="product_price"/> + ''', + }) + + add_to_wishlist = View.create({ + 'name': 'Wishlist', + 'active': True, + 'customize_show': True, + 'inherit_id': products_item.id, + 'key': '_website_sale_wishlist.add_to_wishlist', + 'arch': ''' + <xpath expr="//div[hasclass('product_price')]" position="inside"></xpath> + ''', + }) + + products_list_view = View.create({ + 'name': 'List View', + 'active': False, # <- That's the reason of why this behavior needed a fix + 'customize_show': True, + 'inherit_id': products.id, + 'key': '_website_sale.products_list_view', + 'arch': ''' + <div id="products_grid" position="replace"> + <t t-call="_website_sale.products_item"/> + </div> + ''', + }) + + views = View.with_context(website_id=1).get_related_views('_website_sale.products') + self.assertEqual(views, products + products_item + add_to_wishlist + products_list_view, "The four views should be returned.") + add_to_wishlist.with_context(website_id=1).write({'active': False}) # Trigger cow on hierarchy + add_to_wishlist_cow = Website.with_context(website_id=1).viewref(add_to_wishlist.key) + views = View.with_context(website_id=1).get_related_views('_website_sale.products') + self.assertEqual(views, products + products_item + add_to_wishlist_cow + products_list_view, "The generic wishlist view should have been replaced by the COW one.") + def test_cow_inherit_children_order(self): """ COW method should loop on inherit_children_ids in correct order when copying them on the new specific tree. diff --git a/addons/website/tests/test_website_reset_password.py b/addons/website/tests/test_website_reset_password.py new file mode 100644 index 00000000000..b9fe135d35f --- /dev/null +++ b/addons/website/tests/test_website_reset_password.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +import odoo +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteResetPassword(HttpCase): + + def test_01_website_reset_password_tour(self): + """The goal of this test is to make sure the reset password works.""" + + # We override unlink because we don't want the email to be auto deleted + # if the send works. + MailMail = odoo.addons.mail.models.mail_mail.MailMail + + # We override send_mail because in HttpCase on runbot we don't have an + # SMTP server, so if force_send is set, the test is going to fail. + MailTemplate = odoo.addons.mail.models.mail_template.MailTemplate + original_send_mail = MailTemplate.send_mail + + def my_send_mail(*args, **kwargs): + kwargs.update(force_send=False) + return original_send_mail(*args, **kwargs) + + with patch.object(MailMail, 'unlink', lambda self: None), patch.object(MailTemplate, 'send_mail', my_send_mail): + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('website_reset_password')", "odoo.__DEBUG__.services['web_tour.tour'].tours.website_reset_password.ready", login="admin") diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 60022a477a3..1d2a3822c4c 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -14,6 +14,12 @@ </xpath> </template> +<template id="assets_common" name="Website Assets Common" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website/static/src/js/tours/website_tour_reset_password.js"></script> + </xpath> +</template> + <template id="assets_backend" inherit_id="web.assets_backend" name="Website Backend Assets (used in backend interface)"> <xpath expr="//link[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.backend.scss"/> @@ -867,17 +873,14 @@ </h4> <div id="error_qweb" class="collapse"> <div class="card-body"> - <p t-if="exception.qweb.get('message')"> + <p t-if="exception.message"> <strong>Error message:</strong> - <pre t-esc="exception.qweb.get('message')"/> + <pre t-esc="exception.message"/> </p> <p> - The error occured while rendering the template <code t-esc="qweb_exception.qweb.get('template')"/> - <t t-if="'expression' in qweb_exception.qweb">and evaluating the following expression: <code t-esc="qweb_exception.qweb['expression']"/></t> + The error occured while rendering the template <code t-esc="qweb_exception.name"/> + <t t-if="qweb_exception.html">and evaluating the following expression: <code t-esc="qweb_exception.html"/></t> </p> - <t t-if="'node' in qweb_exception.qweb"> - <pre id="exception_node" t-esc="qweb_exception.pretty_xml()"/> - </t> </div> </div> </div> @@ -941,62 +944,74 @@ </template> <template id="500"> - <!-- This template should not use any variable except those provided by website.ir_http._handle_exception --> + <!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> + <!-- This template should not use any variable except those provided by website.ir_http._handle_exception --> + <!-- no request.crsf_token, no theme style, no assets, ... cursor can be broken during rendering ! --> + <!-- see test_05_reset_specific_view_controller_broken_request --> + <!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> <html> <head> <title t-esc="status_message">Internal Server Error</title> <t t-set="debug" t-value="True"/> - <t t-call-assets="web.assets_common" t-js="false"/> - <t t-call-assets="web.assets_frontend" t-js="false"/> - <t t-call-assets="web.assets_common" t-css="false"/> - <t t-call-assets="web.assets_frontend" t-css="false"/> - <script> - $(document).ready(function() { - var button = $('#reset_templates_button'); - button.click(function() { - var dialog = $('#reset_template_confirmation').modal('show'); - var input = dialog.find('input[type="text"]').val('').focus(); - var dialog_form = dialog.find('form'); - dialog_form.submit(function() { - if (input.val() == dialog.find('.confirm_word').text()) { - dialog.modal('hide'); - button.prop('disabled', true).text('Working...'); - $('#reset_templates_form').trigger('submit'); - } else { - input.val('').focus(); - } + + <link rel="stylesheet" href="/web/static/lib/bootstrap/css/bootstrap.css"/> + <script src="/web/static/lib/jquery/jquery.js" type="text/javascript"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/util.js"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/collapse.js"/> + <script type="text/javascript" src="/web/static/lib/bootstrap/js/modal.js"/> + + <t t-if='views'> + <script> + $(document).ready(function() { + var button = $('#reset_templates_button'); + button.click(function() { + var dialog = $('#reset_template_confirmation').modal('show'); + var input = dialog.find('input[type="text"]').val('').focus(); + var dialog_form = dialog.find('form'); + dialog_form.submit(function() { + if (input.val() == dialog.find('.confirm_word').text()) { + dialog.modal('hide'); + button.prop('disabled', true).text('Working...'); + if ($("input[name='templates']:checked").length) { + $('#reset_templates_form').attr('action', '/website/reset_templates'); + $('#reset_templates_form').trigger('submit'); + } + } else { + input.val('').focus(); + } + return false; + }); return false; }); - return false; }); - }); - </script> + </script> + </t> </head> <body> - <div role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true"> + <div t-if="views" role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true"> <div class="modal-dialog"> <form role="form"> - <div class="modal-content"> - <header class="modal-header"> - <h3 class="modal-title">Reset templates</h3> - <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> - </header> - <main class="modal-body"> - <div class="form-group row mb0"> - <label for="page-name" class="col-md-9 col-form-label"> - <p>The selected templates will be reset to their factory settings.</p> - <p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p> - </label> - <div class="col-md-3 mt16"> - <input type="text" class="form-control" required="required" placeholder="yes"/> + <div class="modal-content"> + <header class="modal-header"> + <h3 class="modal-title">Reset templates</h3> + <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> + </header> + <main class="modal-body"> + <div class="form-group row mb0"> + <label for="page-name" class="col-md-9 col-form-label"> + <p>The selected templates will be reset to their factory settings.</p> + <p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p> + </label> + <div class="col-md-3 mt16"> + <input type="text" id="page-name" class="form-control" required="required" placeholder="yes"/> + </div> </div> - </div> - </main> - <footer class="modal-footer"> - <input type="submit" value="Confirm" class="btn btn-primary"/> - <button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button> - </footer> - </div> + </main> + <footer class="modal-footer"> + <input type="submit" value="Confirm" class="btn btn-primary"/> + <button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button> + </footer> + </div> </form> </div> </div> @@ -1018,18 +1033,20 @@ <div class="oe_structure"> <h1 class="container mt32"><t t-esc="status_code"/>: <t t-esc="status_message"/></h1> </div> - <div class="container" t-if="views"> - <div class="alert alert-danger" t-if="qweb_exception and editable" role="alert"> + <div class="container" t-if="views and editable"> + <div class="alert alert-danger" t-if="debug" role="alert"> <h4>Template fallback</h4> - <p>An error occured while rendering the template <code t-esc="qweb_exception.qweb['template']"/>.</p> + <p>An error occured while rendering the template <code t-esc="qweb_exception.name"/>.</p> <p>If this error is caused by a change of yours in the templates, you have the possibility to reset one or more templates to their <strong>factory settings</strong>.</p> - <form action="/website/reset_templates" method="post" id="reset_templates_form"> - <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> + <form action="#" method="post" id="reset_templates_form"> <ul class="oe_template_fallback"> <li t-foreach="views" t-as="view"> <label> <input type="checkbox" name="templates" t-att-value="view.id" t-att-checked="'checked' if view_first else None"/> <t t-esc="view.name"/> + <span t-if="not view._get_original_view()" class="text-danger"> + (!!! This view will be removed ) + </span> </label> </li> </ul> diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 67272576845..429e8f9d822 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -170,7 +170,7 @@ class WebsiteAccount(CustomerPortal): "website_crm_partner_assign.portal_my_opportunity", { 'opportunity': opp, 'user_activity': opp.sudo().activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1], - 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc'), + 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc, name desc, id desc'), 'activity_types': request.env['mail.activity.type'].sudo().search([]), 'states': request.env['res.country.state'].sudo().search([]), 'countries': request.env['res.country'].sudo().search([]), diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index 08d218f1b02..e8863b571e3 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -510,12 +510,12 @@ </div> <div class="col-md text-md-right"> <div class="d-inline-block"> - <small class="mr-2"><b>Stage:</b></small> - <div t-foreach="stages" t-as="stage" class="float-right"> + <small class="mr-2 mt-1 float-left"><b>Stage:</b></small> + <div t-foreach="stages[::-1]" t-as="stage" class="float-left"> + <i t-if="not stage_first" class="fa fa-chevron-right ml-2 mr-1 small" style="opacity: 0.5"/> <button type="button" t-att-data="stage.id" t-att-opp="opportunity.id" t-attf-class="btn btn-sm px-2 opp-stage-button #{'btn-secondary' if opportunity.stage_id.name != stage.name else 'btn-primary disabled'}"> <span t-field="stage.name"/> </button> - <i t-if="not stage_first" class="fa fa-chevron-right ml-1 mr-2 small" style="opacity: 0.5"/> </div> </div> </div> diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py index 159272983e5..c95d7d29059 100644 --- a/addons/website_event_track/models/event.py +++ b/addons/website_event_track/models/event.py @@ -66,26 +66,26 @@ class Event(models.Model): for event in self: if 'website_track' in vals: if vals['website_track']: - for sequence, (name, url, xml_id, menu_type) in enumerate(self._get_track_menu_entries()): - menu = super(Event, self)._create_menu(sequence, name, url, xml_id) - self.env['website.event.menu'].create({ + for sequence, (name, url, xml_id, menu_type) in enumerate(event._get_track_menu_entries()): + menu = super(Event, event)._create_menu(sequence, name, url, xml_id) + event.env['website.event.menu'].create({ 'menu_id': menu.id, - 'event_id': self.id, + 'event_id': event.id, 'menu_type': menu_type, }) else: - self.track_menu_ids.mapped('menu_id').unlink() + event.track_menu_ids.mapped('menu_id').unlink() if 'website_track_proposal' in vals: if vals['website_track_proposal']: - for sequence, (name, url, xml_id, menu_type) in enumerate(self._get_track_proposal_menu_entries()): - menu = super(Event, self)._create_menu(sequence, name, url, xml_id) - self.env['website.event.menu'].create({ + for sequence, (name, url, xml_id, menu_type) in enumerate(event._get_track_proposal_menu_entries()): + menu = super(Event, event)._create_menu(sequence, name, url, xml_id) + event.env['website.event.menu'].create({ 'menu_id': menu.id, - 'event_id': self.id, + 'event_id': event.id, 'menu_type': menu_type, }) else: - self.track_proposal_menu_ids.mapped('menu_id').unlink() + event.track_proposal_menu_ids.mapped('menu_id').unlink() def _get_track_menu_entries(self): self.ensure_one() diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml index 5cecc496363..59d668f8758 100644 --- a/addons/website_sale/data/mail_template_data.xml +++ b/addons/website_sale/data/mail_template_data.xml @@ -57,7 +57,7 @@ <hr/> % endif <div style="text-align: center; margin: 16px 0px 16px 0px; font-size: 14px;"> - <a href="/shop/cart?access_token=${object.access_token}" + <a href="${object.get_base_url()}/shop/cart?access_token=${object.access_token}" target="_blank" style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"> Resume order diff --git a/addons/website_sale/models/account_invoice.py b/addons/website_sale/models/account_invoice.py index 5696f4ace36..db5e9822889 100644 --- a/addons/website_sale/models/account_invoice.py +++ b/addons/website_sale/models/account_invoice.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import fields, models, api class AccountInvoice(models.Model): @@ -10,3 +10,10 @@ class AccountInvoice(models.Model): website_id = fields.Many2one('website', related='partner_id.website_id', string='Website', help='Website through which this invoice was created.', store=True, readonly=True) + + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(AccountInvoice, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 2492ff27a32..adf71d95eec 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -318,6 +318,7 @@ class SaleOrder(models.Model): @api.multi def action_recovery_email_send(self): + self._portal_ensure_token() composer_form_view_id = self.env.ref('mail.email_compose_message_wizard_form').id try: default_template = self.env.ref('website_sale.mail_template_sale_cart_recovery', raise_if_not_found=False) @@ -344,6 +345,13 @@ class SaleOrder(models.Model): }, } + @api.multi + def get_base_url(self): + """When using multi-website, we want the user to be redirected to the + most appropriate website if possible.""" + res = super(SaleOrder, self).get_base_url() + return self.website_id and self.website_id._get_http_domain() or res + class SaleOrderLine(models.Model): _inherit = "sale.order.line" diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js b/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js new file mode 100644 index 00000000000..7df1d3a7b12 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js @@ -0,0 +1,93 @@ +odoo.define('website_sale.tour_shop_cart_recovery', function (require) { +'use strict'; + +var base = require('web_editor.base'); +var localStorage = require('web.local_storage'); +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); + +var orderIdKey = 'website_sale.tour_shop_cart_recovery.orderId'; +var recoveryLinkKey = 'website_sale.tour_shop_cart_recovery.recoveryLink'; + +tour.register('shop_cart_recovery', { + test: true, + url: '/shop?search=Acoustic Bloc Screens', + wait_for: base.ready(), +}, +[ + { + content: "select Acoustic Bloc Screens", + trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")', + }, + { + content: "click add to cart", + trigger: '#product_details #add_to_cart', + }, + { + content: "check product is in cart, get cart id, logout, go to login", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () { + rpc.query({ + 'model': 'website', + 'method': 'sale_get_order', + 'args': [[$('html').data('website-id')]], + }).then(function (data) { + var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]); + localStorage.setItem(orderIdKey, orderId); + window.location.href = "/web/session/logout?redirect=/web/login"; + }); + }, + }, + { + content: "login as admin and go to the SO (backend)", + trigger: '.oe_login_form', + run: function () { + var orderId = localStorage.getItem(orderIdKey); + var url = "/web#action=sale.action_orders&view_type=form&id=" + orderId; + var $loginForm = $('.oe_login_form'); + $loginForm.find('input[name="login"]').val("admin"); + $loginForm.find('input[name="password"]').val("admin"); + $loginForm.find('input[name="redirect"]').val(url); + $loginForm.submit(); + }, + }, + { + content: "click action", + trigger: '.btn:containsExact("Action")', + }, + { + content: "click Send a Cart Recovery Email", + trigger: 'a:containsExact("Send a Cart Recovery Email")', + }, + { + content: "click Send email", + trigger: '.btn[name="action_send_mail"]', + }, + { + content: "check the mail is sent, grab the recovery link, and logout", + trigger: '.o_thread_message_content a:containsExact("Resume order")', + run: function () { + var link = $('.o_thread_message_content a:containsExact("Resume order")').attr('href'); + localStorage.setItem(recoveryLinkKey, link); + window.location.href = "/web/session/logout?redirect=/"; + } + }, + { + content: "go to the recovery link", + trigger: 'a[href="/web/login"]', + run: function () { + window.location.href = localStorage.getItem(recoveryLinkKey); + }, + }, + { + content: "check the page is working, click on restore", + extra_trigger: 'p:contains("This is your current cart")', + trigger: 'p:contains("restore") a:contains("Click here")', + }, + { + content: "check product is in restored cart", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () {}, + }, +]); +}); diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js new file mode 100644 index 00000000000..c8343ef33e5 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js @@ -0,0 +1,94 @@ +odoo.define('website_sale.tour_shop_mail', function (require) { +'use strict'; + +var base = require('web_editor.base'); +var rpc = require('web.rpc'); +var tour = require('web_tour.tour'); + +tour.register('shop_mail', { + test: true, + url: '/', + wait_for: base.ready(), +}, +[ + { + content: "Change the domain of the websites and go to shop", + trigger: 'body', + run: function () { + // We change the domain of the website to test that the email that + // will be sent uses the correct domain for its links. + var def1 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[1], { + 'domain': "my-test-domain.com", + }], + }); + // We need to change the domain of all the websites otherwise the + // website selector will return the website 2 since the domain we + // set on website 1 doesn't actually match our test server. + var def2 = rpc.query({ + 'model': 'website', + 'method': 'write', + 'args': [[2], { + 'domain': "https://domain-not-used.fr", + }], + }); + $.when(def1, def2).then(function (data) { + window.location.href = '/shop?search=Acoustic Bloc Screens'; + }); + }, + }, + { + content: "select Acoustic Bloc Screens", + trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")', + }, + { + content: "click add to cart", + trigger: '#product_details #add_to_cart', + }, + { + content: "check product is in cart, get cart id, go to backend", + trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")', + run: function () { + rpc.query({ + 'model': 'website', + 'method': 'sale_get_order', + 'args': [[$('html').data('website-id')]], + }).then(function (data) { + var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]); + window.location.href = "/web#action=sale.action_orders&view_type=form&id=" + orderId; + }); + }, + }, + { + content: "click confirm", + trigger: '.btn[name="action_confirm"]', + }, + { + content: "click send by email", + trigger: '.btn[name="action_quotation_send"]', + extra_trigger: '.o_statusbar_status .btn-primary:contains("Sales Order")', + }, + { + content: "click Send email", + trigger: '.btn[name="action_send_mail"]', + }, + { + content: "wait mail to be sent, and go see it", + trigger: '.o_thread_message_content:contains("Here is"):contains("the order")', + run: function () { + window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list"; + }, + }, + { + content: "click on the first email", + trigger: '.o_data_cell:contains("YourCompany Order")', + }, + { + content: "check it's the correct email, and the URL is correct too", + trigger: 'iframe p:contains("Here is"):contains("the order")', + extra_trigger: 'iframe a[href^="http://my-test-domain.com"]', + }, +]); +}); diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index 6a5d755ad64..dde7488cd96 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -597,7 +597,7 @@ td.noborder { } .border_primary { - border: 1px solid $o-brand-odoo; + border: 1px solid theme-color('primary'); } .js_change_shipping { diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index 89fc4f46d6f..fb1a5897f37 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -1,5 +1,7 @@ from . import test_customize from . import test_sale_process +from . import test_website_sale_cart_recovery +from . import test_website_sale_mail from . import test_website_sale_pricelist from . import test_website_sale_product_attribute_value_config from . import test_website_sale_zoom diff --git a/addons/website_sale/tests/test_website_sale_cart_recovery.py b/addons/website_sale/tests/test_website_sale_cart_recovery.py new file mode 100644 index 00000000000..4aaa0433886 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_cart_recovery.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleCartRecovery(HttpCase): + + def test_01_shop_cart_recovery_tour(self): + """The goal of this test is to make sure cart recovery works.""" + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_cart_recovery')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_cart_recovery.ready", login="portal") diff --git a/addons/website_sale/tests/test_website_sale_mail.py b/addons/website_sale/tests/test_website_sale_mail.py new file mode 100644 index 00000000000..8cb495b5db9 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_mail.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +import odoo +from odoo.tests import tagged +from odoo.tests.common import HttpCase + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleMail(HttpCase): + + def test_01_shop_mail_tour(self): + """The goal of this test is to make sure sending SO by email works.""" + + # we override unlink because we don't want the email to be auto deleted + MailMail = odoo.addons.mail.models.mail_mail.MailMail + + with patch.object(MailMail, 'unlink', lambda self: None): + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_mail')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_mail.ready", login="admin") diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 2290b8025f9..6cdaf9fac87 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -46,6 +46,8 @@ <template id="assets_common" name="tour" inherit_id="web.assets_common"> <xpath expr="." position="inside"> <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop.js"></script> + <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js"></script> + <script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_mail.js"></script> </xpath> </template> @@ -1352,7 +1354,7 @@ <t t-set="partner_id" t-value="partner"/> <t t-set="back_button_icon_class" t-value="'fa-chevron-left'"/> - <t t-set="back_button_txt" t-value="'Return to Cart'"/> + <t t-set="back_button_txt">Return to Cart</t> <t t-set="back_button_class" t-value="'btn btn-secondary'"/> <t t-set="back_button_link" t-value="'/shop/cart'"/> </t> diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index bde3430658f..67a45b1aa28 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -5,6 +5,8 @@ var core = require('web.core'); var sAnimations = require('website.content.snippets.animation'); var _t = core._t; +var concurrency = require('web.concurrency'); +var dp = new concurrency.DropPrevious(); sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({ selector: '.oe_website_sale', @@ -87,12 +89,12 @@ sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({ $payButton.prop('disabled', true); $payButton.data('disabled_reasons', $payButton.data('disabled_reasons') || {}); $payButton.data('disabled_reasons').carrier_selection = true; - this._rpc({ + dp.add(this._rpc({ route: '/shop/update_carrier', params: { carrier_id: $(ev.currentTarget).val(), }, - }).then(this._handleCarrierUpdateResult.bind(this)); + })).then(this._handleCarrierUpdateResult.bind(this)); }, /** * @private diff --git a/addons/website_sale_stock/views/website_sale_stock_templates.xml b/addons/website_sale_stock/views/website_sale_stock_templates.xml index 6e755321851..9581154c616 100644 --- a/addons/website_sale_stock/views/website_sale_stock_templates.xml +++ b/addons/website_sale_stock/views/website_sale_stock_templates.xml @@ -37,7 +37,7 @@ <template id="website_sale_stock_product" inherit_id="website_sale.product" priority="4"> <xpath expr="//a[@id='add_to_cart']" position="after"> - <div class='availability_messages'/> + <div class="availability_messages o_not_editable"/> </xpath> </template> diff --git a/doc/cla/corporate/invitu.md b/doc/cla/corporate/invitu.md index 3b15373c298..19207d3de2e 100644 --- a/doc/cla/corporate/invitu.md +++ b/doc/cla/corporate/invitu.md @@ -11,5 +11,6 @@ Signed, Cyril Vinh-Tung contact@invitu.com https://github.com/invitu List of contributors: -Cyril Vinh-Tung contact@invitu.com https://github.com/invitu +Cyril Vinh-Tung cyril@invitu.com https://github.com/cvinh +Tymofii Glukhov tima@invitu.com https://github.com/tglukhov diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md index 512104ee2a6..235931654a6 100644 --- a/doc/cla/corporate/vauxoo.md +++ b/doc/cla/corporate/vauxoo.md @@ -44,3 +44,4 @@ Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906 Erick Birbe erick@vauxoo.com https://github.com/ebirbe Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro +Arturo Flores arturo@vauxoo.com https://github.com/umiphos diff --git a/doc/cla/individual/naglis.md b/doc/cla/individual/naglis.md new file mode 100644 index 00000000000..dde081dfaa1 --- /dev/null +++ b/doc/cla/individual/naglis.md @@ -0,0 +1,11 @@ +Lithuania, 2019-02-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Naglis Jonaitis naglis@mailbox.org https://github.com/naglis diff --git a/doc/reference/reports.rst b/doc/reference/reports.rst index 372be4ff34a..ee36f9bfd27 100644 --- a/doc/reference/reports.rst +++ b/doc/reference/reports.rst @@ -301,6 +301,38 @@ the function ``render_html`` and pass objects in the ``docargs`` dictionary: } return report_obj.render('module.report_name', docargs) +.. _reference/reports/custom_fonts: + +Custom fonts +============ +If you want to use custom fonts you will need to add your custom font and the related less/CSS to the ``web.reports_assets_common`` assets bundle. +Adding your custom font(s) to ``web.assets_common`` or ``web.assets_backend`` will not make your font available in QWeb reports. + +Example:: + + <template id="report_assets_common_custom_fonts" name="Custom QWeb fonts" inherit_id="web.report_assets_common"> + <xpath expr="." position="inside"> + <link href="/your_module/static/src/less/fonts.less" rel="stylesheet" type="text/less"/> + </xpath> + </template> + +You will need to define your ``@font-face`` within this less file, even if you've used in another assets bundle (other than ``web.reports_assets_common``). + +Example:: + + @font-face { + font-family: 'MonixBold'; + src: local('MonixBold'), local('MonixBold'), url(/your_module/static/src/fonts/MonixBold-Regular.otf) format('opentype'); + } + + .h1-title-big { + font-family: MonixBold; + font-size: 60px; + color: #3399cc; + } + +After you've added the less into your assets bundle you can use the classes - in this example ``h1-title-big`` - in your custom QWeb report. + Reports are web pages ===================== diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv index 326448849a0..9fc3aa1c861 100644 --- a/odoo/addons/base/data/res.lang.csv +++ b/odoo/addons/base/data/res.lang.csv @@ -1,8 +1,10 @@ "id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","translatable","week_start" "base.lang_en","English","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","True","7" "base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","True","7" -"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" +"base.lang_ar","Arabic / الْعَرَبيّة","ar_AA","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" +"base.lang_ar_SY","Arabic (Syria) / الْعَرَبيّة","ar_SY","ar_SY","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" "base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","True","1" +"base.lang_bn_IN","Bengali / বাংলা","bn_IN","bn_IN","Left-to-Right","[]",",",,"%A %d %b %Y","%I:%M:%S","True","1" "base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","7" "base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","True","1" "base.lang_ca_ES","Catalan / Català","ca_ES","ca_ES","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","1" diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 095dcabcf1c..4136b4348f6 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -467,7 +467,10 @@ class IrActionsReport(models.Model): ) return barcode.asString('png') except (ValueError, AttributeError): - raise ValueError("Cannot convert into barcode.") + if barcode_type == 'Code128': + raise ValueError("Cannot convert into barcode.") + else: + return self.barcode('Code128', value, width=width, height=height, humanreadable=humanreadable) @api.multi def render_template(self, template, values=None): diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 5fd437e9498..b2e11f85623 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -251,7 +251,7 @@ actual arch. for view in self: arch_fs = None xml_id = view.xml_id or view.key - if 'xml' in config['dev_mode'] and view.arch_fs and xml_id: + if (self._context.get('read_arch_from_file') or 'xml' in config['dev_mode']) and view.arch_fs and xml_id: # It is safe to split on / herebelow because arch_fs is explicitely stored with '/' fullpath = get_resource_path(*view.arch_fs.split('/')) if fullpath: @@ -766,19 +766,20 @@ actual arch. [view_data] = root.read(fields=fields) view_arch = etree.fromstring(view_data['arch'].encode('utf-8')) if not root.inherit_id: + if self._context.get('inherit_branding'): + view_arch.attrib.update({ + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(root.id), + 'data-oe-field': 'arch', + }) arch_tree = view_arch else: + if self._context.get('inherit_branding'): + self.inherit_branding(view_arch, root.id, root.id) parent_view = root.inherit_id.read_combined(fields=fields) arch_tree = etree.fromstring(parent_view['arch']) arch_tree = self.apply_inheritance_specs(arch_tree, view_arch, parent_view['id']) - if self._context.get('inherit_branding'): - arch_tree.attrib.update({ - 'data-oe-model': 'ir.ui.view', - 'data-oe-id': str(root.id), - 'data-oe-field': 'arch', - }) - # and apply inheritance arch = self.apply_view_inheritance(arch_tree, root.id, self.model) @@ -1196,6 +1197,7 @@ actual arch. def _contains_branded(self, node): return node.tag == 't'\ or 't-raw' in node.attrib\ + or 't-call' in node.attrib\ or any(self.is_node_branded(child) for child in node.iterdescendants()) def _pop_view_branding(self, element): diff --git a/odoo/addons/base/models/res_country.py b/odoo/addons/base/models/res_country.py index fe86446bfbc..8fd1f978ed0 100644 --- a/odoo/addons/base/models/res_country.py +++ b/odoo/addons/base/models/res_country.py @@ -121,7 +121,7 @@ class CountryState(models.Model): @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] - if 'country_id' in self.env.context: + if self.env.context.get('country_id'): args = expression.AND([args, [('country_id', '=', self.env.context.get('country_id'))]]) if operator == 'ilike' and not (name or '').strip(): diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index c558c8f7920..885a3c6bf09 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -867,6 +867,12 @@ class Partner(models.Model): def _get_country_name(self): return self.country_id.name or '' + @api.multi + def get_base_url(self): + """Get the base URL for the current partner.""" + self.ensure_one() + return self.env['ir.config_parameter'].sudo().get_param('web.base.url') + class ResPartnerIndustry(models.Model): _description = 'Industry' diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 0a1f3060762..cb16873b1a3 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -674,6 +674,51 @@ class TestTemplating(ViewCase): second.get('data-oe-id'), "second should come from the extension view") + def test_branding_primary_inherit(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<root> + <item order="1"/> + </root> + """ + }) + view2 = self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'mode': 'primary', + 'inherit_id': view1.id, + 'arch': """<xpath expr="//item" position="after"> + <item order="2"/> + </xpath> + """ + }) + + arch_string = view2.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + [initial] = arch.xpath('//item[@order=1]') + self.assertEqual( + initial.get('data-oe-id'), + str(view1.id), + "initial should come from the root view") + self.assertEqual( + initial.get('data-oe-xpath'), + '/root[1]/item[1]', + "initial's xpath should be within the inherited view only") + + [second] = arch.xpath('//item[@order=2]') + self.assertEqual( + second.get('data-oe-id'), + str(view2.id), + "second should come from the extension view") + self.assertEqual( + second.get('data-oe-xpath'), + '/xpath/item', + "second xpath should be on the inheriting view only") + def test_branding_distribute_inner(self): """ Checks that the branding is correctly distributed within a view extension @@ -724,6 +769,21 @@ class TestTemplating(ViewCase): ) ) + def test_call_no_branding(self): + view = self.View.create({ + 'name': "Base View", + 'type': 'qweb', + 'arch': """<root> + <item><span t-call="foo"/></item> + </root>""", + }) + + arch_string = view.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + self.assertEqual(arch, E.root(E.item(E.span({'t-call': "foo"})))) + def test_esc_no_branding(self): view = self.View.create({ 'name': "Base View", diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 704c85a9dd7..64bf5da075a 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -369,6 +369,26 @@ class TestFields(common.TransactionCase): self.assertEqual(record.bar3, 'C') self.assertCountEqual(log, ['compute']) + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + def test_14_search(self): """ test search on computed fields """ discussion = self.env.ref('test_new_api.discussion_0') @@ -771,6 +791,10 @@ class TestFields(common.TransactionCase): self.env['ir.property'].create({'name': 'foo', 'fields_id': field_tag_id.id, 'value': tag0, 'type': 'many2one'}) + # assumption: users don't have access to 'ir.property' + accesses = self.env['ir.model.access'].search([('model_id.model', '=', 'ir.property')]) + accesses.write(dict.fromkeys(['perm_read', 'perm_write', 'perm_create', 'perm_unlink'], False)) + # create/modify a record, and check the value for each user record = self.env['test_new_api.company'].create({ 'foo': 'main', @@ -824,6 +848,13 @@ class TestFields(common.TransactionCase): self.assertEqual(record.sudo(user1).foo, False) self.assertEqual(record.sudo(user2).foo, 'default') + # set field with 'force_company' in context + record.sudo(user0).with_context(force_company=company1.id).foo = 'beta' + record.invalidate_cache() + self.assertEqual(record.sudo(user0).foo, 'main') + self.assertEqual(record.sudo(user1).foo, 'beta') + self.assertEqual(record.sudo(user2).foo, 'default') + # create company record and attribute company_record = self.env['test_new_api.company'].create({'foo': 'ABC'}) attribute_record = self.env['test_new_api.company.attr'].create({ @@ -843,6 +874,25 @@ class TestFields(common.TransactionCase): self.assertEqual(attribute_record.bar, 'DEFDEF') self.assertFalse(self.env.has_todo()) + # add group on company-dependent field + self.assertFalse(user0.has_group('base.group_system')) + self.patch(type(record).foo, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + + user0.write({'groups_id': [(4, self.env.ref('base.group_system').id)]}) + record.sudo(user0).foo = 'yes we can' + + # add ir.rule to prevent access on record + self.assertTrue(user0.has_group('base.group_user')) + rule = self.env['ir.rule'].create({ + 'model_id': self.env['ir.model']._get_id(record._name), + 'groups': [self.env.ref('base.group_user').id], + 'domain_force': str([('id', '!=', record.id)]), + }) + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + def test_30_read(self): """ test computed fields as returned by read(). """ discussion = self.env.ref('test_new_api.discussion_0') diff --git a/odoo/addons/test_testing_utilities/menu.xml b/odoo/addons/test_testing_utilities/menu.xml index 99e409708c2..3ca18ef2a50 100644 --- a/odoo/addons/test_testing_utilities/menu.xml +++ b/odoo/addons/test_testing_utilities/menu.xml @@ -10,6 +10,17 @@ name="O2M onchange test thing" action="action_test_o2m_onchange"/> + <record id="action_test_m2o_onchange" model="ir.actions.act_window"> + <field name="name">test_default_and_onchange</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">test_testing_utilities.d</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem id="menu_m2o" parent="base.menu_tests" + name="test_default_and_onchange" + action="action_test_m2o_onchange"/> + <record id="o2m_parent" model="ir.ui.view"> <field name="name">Parent External Form</field> <field name="priority">1</field> @@ -98,7 +109,7 @@ <field name="arch" type="xml"> <form> <field name="f1"/> - <field name="f2" attrs="{'readonly': [['f1', '=', 63], ['f3', '=', 5]]}"/> + <field name="f2" attrs="{'readonly': [['f1', '=', '63'], ['f3', '=', 5]]}"/> <field name="f3"/> <field name="f4"/> <field name="f5" readonly="1"/> diff --git a/odoo/addons/test_testing_utilities/models.py b/odoo/addons/test_testing_utilities/models.py index 98a9fea7e7f..ffb27cd08d8 100644 --- a/odoo/addons/test_testing_utilities/models.py +++ b/odoo/addons/test_testing_utilities/models.py @@ -6,7 +6,7 @@ class A(models.Model): _name = 'test_testing_utilities.a' _description = 'Testing Utilities A' - f1 = fields.Integer(required=True) + f1 = fields.Char(required=True) f2 = fields.Integer(default=42) f3 = fields.Integer() f4 = fields.Integer(compute='_compute_f4') @@ -22,7 +22,7 @@ class A(models.Model): @api.depends('f1', 'f2') def _compute_f4(self): for r in self: - r.f4 = r.f2 / (r.f1 or 1) + r.f4 = r.f2 / (int(r.f1) or 1) class B(models.Model): _name = 'test_testing_utilities.readonly' @@ -154,7 +154,6 @@ class O2MSub(models.Model): @api.onchange('has_parent') def _onchange_has_parent(self): - self.has_parent = bool(self.parent_id) if self.has_parent: self.value = self.parent_id.value diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py index e2f0d0d89d3..344ae62f4d2 100644 --- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py +++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py @@ -24,7 +24,7 @@ class TestBasic(TransactionCase): self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 42) - f.f1 = 4 + f.f1 = '4' self.assertEqual(f.f2, 42) self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 10) @@ -36,7 +36,7 @@ class TestBasic(TransactionCase): r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), - (4, 8, 4, 2), + ('4', 8, 4, 2), ) def test_required(self): @@ -45,12 +45,12 @@ class TestBasic(TransactionCase): with self.assertRaisesRegexp(AssertionError, 'f1 is a required field'): f.save() # set f1 and unset f2 => should work - f.f1 = 1 + f.f1 = '1' f.f2 = False r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), - (1, 0, 0, 0) + ('1', 0, 0, 0) ) def test_readonly(self): @@ -61,7 +61,7 @@ class TestBasic(TransactionCase): f = Form(self.env['test_testing_utilities.readonly']) with self.assertRaises(AssertionError): - f.f1 = 5 + f.f1 = '5' with self.assertRaises(AssertionError): f.f2 = 42 @@ -70,7 +70,7 @@ class TestBasic(TransactionCase): """ f = Form(self.env['test_testing_utilities.a'], view='test_testing_utilities.non_normalized_attrs') - f.f1 = 1 + f.f1 = '1' f.f2 = 987 self.assertEqual(f.f5, 987) self.assertEqual(f.f6, 987) @@ -86,7 +86,7 @@ class TestBasic(TransactionCase): # not readonly yet, should work f.f2 = 5 # make f2 readonly - f.f1 = 63 + f.f1 = '63' f.f3 = 5 with self.assertRaises(AssertionError): f.f2 = 6 @@ -101,9 +101,9 @@ class TestM2O(TransactionCase): f = Form(self.env['test_testing_utilities.d']) - self.assertEqual( - f.f, a, - "The default value for the m2o should be the first Sub record" + self.assertFalse( + f.f, + "The default value gets overridden by the onchange" ) f.f2 = "B" self.assertEqual( @@ -271,6 +271,7 @@ class TestO2M(TransactionCase): [get(s) for s in r.subs], [("2", 2, 2), ("2", 2, 2)] ) + self.assertEqual(r.v, 5) with Form(r, view='test_testing_utilities.o2m_parent') as f: with f.subs.new() as sub: @@ -286,6 +287,7 @@ class TestO2M(TransactionCase): [get(s) for s in r.subs], [("2", 2, 2), ("5", 5, 5), ("2", 2, 2)] ) + self.assertEqual(r.v, 10) with Form(r, view='test_testing_utilities.o2m_parent') as f, \ f.subs.edit(index=0) as sub,\ @@ -439,12 +441,12 @@ class TestEdition(TransactionCase): """ def test_trivial(self): r = self.env['test_testing_utilities.a'].create({ - 'f1': 5, + 'f1': '5', }) with Form(r) as f: self.assertEqual(f.id, r.id) - self.assertEqual(f.f1, 5) + self.assertEqual(f.f1, '5') self.assertEqual(f.f4, 8) f.f2 = 5 diff --git a/odoo/api.py b/odoo/api.py index dffbc0e633a..b22d9ac2476 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -1036,7 +1036,7 @@ class Cache(object): """ Return the value of ``field`` for ``record``. """ key = record.env.cache_key(field) try: - value = self._data[key][field][record.id] + value = self._data[key][field][record._ids[0]] except KeyError: raise CacheMiss(record, field) @@ -1045,7 +1045,12 @@ class Cache(object): def set(self, record, field, value): """ Set the value of ``field`` for ``record``. """ key = record.env.cache_key(field) - self._data[key][field][record.id] = value + self._data[key][field][record._ids[0]] = value + + def update(self, records, field, values): + """ Set the values of ``field`` for several ``records``. """ + key = records.env.cache_key(field) + self._data[key][field].update(zip(records._ids, values)) def remove(self, record, field): """ Remove the value of ``field`` for ``record``. """ diff --git a/odoo/fields.py b/odoo/fields.py index a0a08631c52..75f6097d1aa 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -643,13 +643,25 @@ class Field(MetaField('DummyField', (object,), {})): return model.env['ir.property'].get(self.name, self.model_name) def _compute_company_dependent(self, records): - Property = records.env['ir.property'] + # read property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = Property.get_multi(self.name, self.model_name, records.ids) for record in records: record[self.name] = values.get(record.id) def _inverse_company_dependent(self, records): - Property = records.env['ir.property'] + # update property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = { record.id: self.convert_to_write(record[self.name], record) for record in records @@ -2127,7 +2139,8 @@ class Many2one(_Relational): return () def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): if use_name_get and value: @@ -2247,7 +2260,8 @@ class _RelationalMulti(_Relational): raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): return value.ids diff --git a/odoo/models.py b/odoo/models.py index 9104406195b..b936cf8ae02 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2779,20 +2779,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # retrieve results from records; this takes values from the cache and # computes remaining fields self = self.with_prefetch(self._prefetch.copy()) - data = {record: {'id': record.id} for record in self} - missing = set() + data = [(record, {'id': record._ids[0]}) for record in self] use_name_get = (load == '_classic_read') for name in (stored + inherited + computed): convert = self._fields[name].convert_to_read # restrict the prefetching of self's model to self; this avoids # computing fields on a larger recordset than self self._prefetch[self._name] = set(self._ids) - for record in self: + for record, vals in data: + # missing records have their vals empty + if not vals: + continue try: - data[record][name] = convert(record[name], record, use_name_get) + vals[name] = convert(record[name], record, use_name_get) except MissingError: - missing.add(record) - result = [data[record] for record in self if record not in missing] + vals.clear() + result = [vals for record, vals in data if vals] return result @@ -2887,36 +2889,46 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): res = 'pg_size_pretty(length(%s)::bigint)' % res return '%s as "%s"' % (res, col) + # selected fields are: 'id' followed by fields_pre qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre] # determine the actual query to execute from_clause, where_clause, params = query.get_sql() query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause) - result = [] + # fetch one list of record values per field + field_values_list = [[] for name in qual_names] param_pos = params.index(param_ids) for sub_ids in cr.split_for_in_conditions(self.ids): params[param_pos] = tuple(sub_ids) cr.execute(query_str, params) - result.extend(cr.dictfetchall()) + for row in cr.fetchall(): + for values, val in zip(field_values_list, row): + values.append(val) - ids = [vals['id'] for vals in result] + ids = field_values_list.pop(0) fetched = self.browse(ids) if ids: # translate the fields if necessary if context.get('lang'): - for field in fields_pre: + for field, values in zip(fields_pre, field_values_list): if not field.inherited and callable(field.translate): name = field.name translate = field.get_trans_func(fetched) - for vals in result: - vals[name] = translate(vals['id'], vals[name]) + for index in range(len(ids)): + values[index] = translate(ids[index], values[index]) # store result in cache - for vals in result: - record = self.browse(vals.pop('id'), self._prefetch) - record._cache.update(record._convert_to_cache(vals, validate=False)) + target = self.browse([], self._prefetch) + for field, values in zip(fields_pre, field_values_list): + convert = field.convert_to_cache + # Note that the target record passed to convert below is empty. + # This does not harm in practice, as it is only used in Monetary + # fields for rounding the value. As the value comes straight + # from the database, it is expected to be rounded already. + values = [convert(value, target, validate=False) for value in values] + self.env.cache.update(fetched, field, values) # determine the fields that must be processed now; # for the sake of simplicity, we ignore inherited fields @@ -3278,6 +3290,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): with self.env.protecting(protected_fields, self): # write stored fields with (low-level) method _write if store_vals or inverse_vals or inherited_vals: + # if log_access is enabled, this updates 'write_date' and + # 'write_uid' and check access rules, even when old_vals is + # empty self._write(store_vals) # update parent records (after possibly updating parent fields) @@ -3294,6 +3309,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): self.env[model_name].browse(parent_ids).write(parent_vals) if inverse_vals: + self.check_field_access_rights('write', list(inverse_vals)) + self.modified(set(inverse_vals) - set(store_vals)) # group fields by inverse method (to call it once), and order @@ -4624,7 +4641,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # @classmethod - def _browse(cls, ids, env, prefetch=None): + def _browse(cls, ids, env, prefetch=None, add_prefetch=True): """ Create a recordset instance. :param ids: a tuple of record ids @@ -4637,7 +4654,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if prefetch is None: prefetch = defaultdict(set) # {model_name: set(ids)} records._prefetch = prefetch - prefetch[cls._name].update(ids) + if add_prefetch: + prefetch[cls._name].update(ids) return records def browse(self, arg=None, prefetch=None): @@ -4676,9 +4694,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ Verifies that the current recorset holds a single record. Raises an exception otherwise. """ - if len(self) == 1: + try: + # unpack to ensure there is only one value is faster than len when true and + # has a significant impact as this check is largely called + _id, = self._ids return self - raise ValueError("Expected singleton: %s" % self) + except ValueError: + raise ValueError("Expected singleton: %s" % self) def with_env(self, env): """ Returns a new version of this recordset attached to the provided @@ -4944,7 +4966,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): def __iter__(self): """ Return an iterator over ``self``. """ for id in self._ids: - yield self._browse((id,), self.env, self._prefetch) + yield self._browse((id,), self.env, self._prefetch, False) def __contains__(self, item): """ Test whether ``item`` (record or field name) is an element of ``self``. diff --git a/odoo/tests/common.py b/odoo/tests/common.py index c7bbd0cebb7..9072cb14c10 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -1198,6 +1198,11 @@ class Form(object): if fields[k]['type'] == 'one2many': # o2m default gets a (6) at the start, makes no sense return [c for c in v if c[0] != 6] + elif fields[k]['type'] == 'datetime' and isinstance(v, datetime): + return odoo.fields.Datetime.to_string(v) + elif fields[k]['type'] == 'date' and isinstance(v, date): + return odoo.fields.Datetime.to_string(v) + return v defaults = { k: cleanup(k, v) @@ -1213,9 +1218,12 @@ class Form(object): vals[k] = [(6, False, [])] elif type_ == 'one2many': vals[k] = [] + elif type_ in ('integer', 'float'): + vals[k] = 0 - # TODO: check that only fields with default values should be sent - self._perform_onchange(list(defaults.keys())) + # on creation, every field is considered changed by the client + # apparently + self._perform_onchange(list(vals.keys() - {'id'})) def _init_from_values(self, values): self._values.update( @@ -1369,20 +1377,22 @@ class Form(object): node = self._get_node(f) if not node.get('force_save'): continue - # TODO: filter out (1, _, {}) from o2m values + values[f] = v return values def _perform_onchange(self, fields): assert isinstance(fields, list) - - # marks any onchange source as changed (default_get or explicit set) + # marks any onchange source as changed self._changed.update(fields) - result = self._model.onchange( - self._onchange_values(), - fields, - self._view['onchange'], - ) + + # skip calling onchange() if there's no trigger on any of the changed + # fields + spec = self._view['onchange'] + if not any(spec[f] for f in fields): + return + + result = self._model.onchange(self._onchange_values(), fields, spec) if result.get('warning'): _logger.getChild('onchange').warn("%(title)s %(message)s" % result.get('warning')) values = result.get('value', {}) @@ -1398,7 +1408,18 @@ class Form(object): ) def _onchange_values(self): - return dict(self._values) + f = self._view['fields'] + values = {} + for k, v in self._values.items(): + if f[k]['type'] == 'one2many': + # web client sends a 4 for unmodified o2m rows + values[k] = [ + (4, rid, False) if (c == 1 and not vs) else (c, rid, vs) + for (c, rid, vs) in v + ] + else: + values[k] = v + return values def _cleanup_onchange(self, descr, value, current): if descr['type'] == 'many2one': @@ -1412,6 +1433,7 @@ class Form(object): return [] v = [] + c = {t[1]: t[2] for t in current if t[0] == 1} # which view should this be??? subfields = descr['views']['edition']['fields'] for command in value: @@ -1424,7 +1446,10 @@ class Form(object): for k, v in command[2].items() if k in subfields })) - # TODO: should reuse existing values if not 5? + elif command[0] == 4: + v.append((1, command[1], c.get(command[1], {}))) + elif command[0] == 5: + v = [] return v elif descr['type'] == 'many2many': # onchange result is a bunch of commands, normalize to single 6 @@ -1444,7 +1469,7 @@ class Form(object): else: raise ValueError( "Unsupported M2M command %d" % command[0]) - return [(6, 0, ids)] + return [(6, False, ids)] return value @@ -1508,7 +1533,6 @@ class O2MForm(Form): continue # if self._get_modifier(f, 'readonly'): # continue - # TODO: filter out (1, _, {}) from o2m values values[f] = v return values @@ -1541,7 +1565,10 @@ class O2MProxy(X2MProxy): elif command == 2: pass else: - raise AssertionError("O2M proxy only supports commands 0, 1 and 2") + raise AssertionError("O2M proxy only supports commands 0, 1 and 2, found %s" % command) + + def __len__(self): + return len(self._records) @property def _model(self): @@ -1703,6 +1730,10 @@ def record_to_values(fields, record): v = [(6, 0, v.ids)] elif descr['type'] == 'one2many': v = [(1, r.id, {}) for r in v] + elif descr['type'] == 'datetime' and isinstance(v, datetime): + v = odoo.fields.Datetime.to_string(v) + elif descr['type'] == 'date' and isinstance(v, date): + v = odoo.fields.Date.to_string(v) r[f] = v return r |
