summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-02-21 15:37:19 +0100
committerChristophe Simonis <chs@odoo.com>2019-02-21 15:37:19 +0100
commit1d2b6f1bef6af13245d29d56cbb5f9d61f9ca038 (patch)
tree8ea1d203aef83d287de9ec578f81bd64eedb7359
parentf945b1bcc21621cfc056e40f4d66b7181d19239e (diff)
parent84143a34b313c01b0b37b7d7246ce3d5c9476002 (diff)
[MERGE] forward port branch 12.0 up to 84143a34b31
-rw-r--r--addons/account/models/account.py2
-rw-r--r--addons/account/models/account_bank_statement.py5
-rw-r--r--addons/account/models/account_move.py229
-rw-r--r--addons/account/models/account_payment.py2
-rw-r--r--addons/account/models/chart_template.py2
-rw-r--r--addons/account/security/ir.model.access.csv2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js4
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js2
-rw-r--r--addons/account/static/src/xml/account_reconciliation.xml3
-rw-r--r--addons/account/tests/account_test_no_chart.py6
-rw-r--r--addons/account/tests/test_reconciliation.py8
-rw-r--r--addons/account/views/res_config_settings_views.xml1
-rw-r--r--addons/account_facturx/models/account_invoice.py19
-rw-r--r--addons/account_voucher/models/account_voucher.py35
-rw-r--r--addons/auth_signup/models/res_partner.py2
-rw-r--r--addons/base_automation/views/base_automation_view.xml3
-rw-r--r--addons/base_import_module/views/base_import_module_view.xml1
-rw-r--r--addons/bus/static/src/js/longpolling_bus.js1
-rw-r--r--addons/crm/models/crm_lead.py2
-rw-r--r--addons/crm/views/crm_lead_views.xml2
-rw-r--r--addons/digest/data/digest_template_data.xml5
-rw-r--r--addons/digest/models/digest.py2
-rw-r--r--addons/google_calendar/models/google_calendar.py4
-rw-r--r--addons/hr/views/hr_views.xml11
-rw-r--r--addons/hr_attendance/static/src/js/kiosk_confirm.js10
-rw-r--r--addons/hr_attendance/static/src/js/my_attendances.js5
-rw-r--r--addons/hr_holidays/models/hr_leave_type.py5
-rw-r--r--addons/hr_payroll_account/models/hr_payroll_account.py14
-rw-r--r--addons/l10n_ch/i18n_extra/de.po471
-rw-r--r--addons/l10n_ch/i18n_extra/it.po612
-rw-r--r--addons/l10n_ch/i18n_extra/l10n_ch.pot583
-rw-r--r--addons/l10n_it/__manifest__.py2
-rw-r--r--addons/l10n_it_edi/__init__.py5
-rw-r--r--addons/l10n_it_edi/__manifest__.py22
-rw-r--r--addons/l10n_it_edi/data/account_invoice_demo.xml23
-rw-r--r--addons/l10n_it_edi/data/invoice_it_template.xml175
-rw-r--r--addons/l10n_it_edi/models/__init__.py8
-rw-r--r--addons/l10n_it_edi/models/account_invoice.py752
-rw-r--r--addons/l10n_it_edi/models/ddt.py21
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py358
-rw-r--r--addons/l10n_it_edi/models/res_company.py110
-rw-r--r--addons/l10n_it_edi/models/res_partner.py28
-rw-r--r--addons/l10n_it_edi/security/ir.model.access.csv2
-rw-r--r--addons/l10n_it_edi/tests/__init__.py2
-rw-r--r--addons/l10n_it_edi/tests/test_generate.py32
-rw-r--r--addons/l10n_it_edi/views/l10n_it_view.xml265
-rw-r--r--addons/l10n_mx/data/account_data.xml4
-rw-r--r--addons/l10n_mx/data/account_tax_data.xml34
-rw-r--r--addons/mail/data/mail_data.xml4
-rw-r--r--addons/mail/models/mail_message.py2
-rw-r--r--addons/mail/models/mail_thread.py4
-rw-r--r--addons/mail/static/src/js/chatter.js9
-rw-r--r--addons/mail/static/src/js/thread_widget.js1
-rw-r--r--addons/mail/static/src/xml/chatter.xml8
-rw-r--r--addons/mass_mailing/models/mass_mailing.py6
-rw-r--r--addons/note/static/src/js/systray_activity_menu.js2
-rw-r--r--addons/note/static/tests/systray_activity_menu_tests.js1
-rw-r--r--addons/point_of_sale/models/pos_order.py10
-rw-r--r--addons/point_of_sale/models/pos_session.py2
-rw-r--r--addons/point_of_sale/static/src/js/gui.js6
-rw-r--r--addons/point_of_sale/static/src/js/screens.js4
-rw-r--r--addons/point_of_sale/views/pos_templates.xml6
-rw-r--r--addons/portal/models/portal_mixin.py10
-rw-r--r--addons/portal/wizard/portal_share.py10
-rw-r--r--addons/project/models/project.py14
-rw-r--r--addons/project/static/src/js/tour.js5
-rw-r--r--addons/purchase_requisition/data/purchase_requisition_data.xml2
-rw-r--r--addons/sale/models/sale.py8
-rw-r--r--addons/sale_crm/models/crm_lead.py2
-rw-r--r--addons/sale_margin/models/sale_order.py2
-rw-r--r--addons/sale_quotation_builder/views/sale_portal_templates.xml6
-rw-r--r--addons/sale_stock/views/sale_stock_portal_template.xml2
-rw-r--r--addons/stock/models/product.py4
-rw-r--r--addons/stock/report/report_deliveryslip.xml12
-rw-r--r--addons/stock/views/stock_picking_views.xml7
-rw-r--r--addons/test_mail/tests/test_performance.py2
-rw-r--r--addons/test_website/__init__.py4
-rw-r--r--addons/test_website/__manifest__.py24
-rw-r--r--addons/test_website/controllers/__init__.py4
-rw-r--r--addons/test_website/controllers/main.py13
-rw-r--r--addons/test_website/data/test_website_data.xml54
-rw-r--r--addons/test_website/static/src/js/reset_views.test.js103
-rw-r--r--addons/test_website/tests/__init__.py4
-rw-r--r--addons/test_website/tests/test_reset_views.py102
-rw-r--r--addons/test_website/views/templates.xml8
-rw-r--r--addons/web/static/lib/moment/locale/es.js20
-rw-r--r--addons/web/static/src/js/core/abstract_storage_service.js11
-rw-r--r--addons/web/static/src/js/core/utils.js12
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js1
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_filters.js2
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js4
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js8
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js3
-rw-r--r--addons/web/static/src/scss/form_view.scss4
-rw-r--r--addons/web/static/src/xml/base.xml2
-rw-r--r--addons/web/static/tests/chrome/user_menu_tests.js1
-rw-r--r--addons/web/static/tests/core/util_tests.js22
-rw-r--r--addons/web/static/tests/core/widget_tests.js24
-rw-r--r--addons/web/static/tests/helpers/test_utils_mock.js5
-rw-r--r--addons/web/static/tests/views/abstract_controller_tests.js2
-rw-r--r--addons/web/static/tests/views/pivot_tests.js108
-rw-r--r--addons/web_editor/models/ir_ui_view.py15
-rw-r--r--addons/web_editor/static/src/scss/web_editor.ui.scss4
-rw-r--r--addons/web_editor/static/src/scss/web_editor.variables.scss38
-rw-r--r--addons/web_editor/static/src/xml/editor.xml8
-rw-r--r--addons/website/controllers/main.py36
-rw-r--r--addons/website/models/ir_http.py68
-rw-r--r--addons/website/models/ir_ui_view.py19
-rw-r--r--addons/website/models/res_partner.py7
-rw-r--r--addons/website/models/website.py75
-rw-r--r--addons/website/static/src/js/tours/website_tour_reset_password.js181
-rw-r--r--addons/website/static/src/scss/website.ui.scss8
-rw-r--r--addons/website/static/src/xml/website.pageProperties.xml4
-rw-r--r--addons/website/tests/__init__.py2
-rw-r--r--addons/website/tests/test_get_current_website.py114
-rw-r--r--addons/website/tests/test_views.py76
-rw-r--r--addons/website/tests/test_website_reset_password.py34
-rw-r--r--addons/website/views/website_templates.xml129
-rw-r--r--addons/website_crm_partner_assign/controllers/main.py2
-rw-r--r--addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml6
-rw-r--r--addons/website_event_track/models/event.py20
-rw-r--r--addons/website_sale/data/mail_template_data.xml2
-rw-r--r--addons/website_sale/models/account_invoice.py9
-rw-r--r--addons/website_sale/models/sale_order.py8
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop_cart_recovery.js93
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop_mail.js94
-rw-r--r--addons/website_sale/static/src/scss/website_sale.scss2
-rw-r--r--addons/website_sale/tests/__init__.py2
-rw-r--r--addons/website_sale/tests/test_website_sale_cart_recovery.py13
-rw-r--r--addons/website_sale/tests/test_website_sale_mail.py24
-rw-r--r--addons/website_sale/views/templates.xml4
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js6
-rw-r--r--addons/website_sale_stock/views/website_sale_stock_templates.xml2
-rw-r--r--doc/cla/corporate/invitu.md3
-rw-r--r--doc/cla/corporate/vauxoo.md1
-rw-r--r--doc/cla/individual/naglis.md11
-rw-r--r--doc/reference/reports.rst32
-rw-r--r--odoo/addons/base/data/res.lang.csv4
-rw-r--r--odoo/addons/base/models/ir_actions_report.py5
-rw-r--r--odoo/addons/base/models/ir_ui_view.py18
-rw-r--r--odoo/addons/base/models/res_country.py2
-rw-r--r--odoo/addons/base/models/res_partner.py6
-rw-r--r--odoo/addons/base/tests/test_views.py60
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py50
-rw-r--r--odoo/addons/test_testing_utilities/menu.xml13
-rw-r--r--odoo/addons/test_testing_utilities/models.py5
-rw-r--r--odoo/addons/test_testing_utilities/tests/test_form_impl.py26
-rw-r--r--odoo/api.py9
-rw-r--r--odoo/fields.py22
-rw-r--r--odoo/models.py62
-rw-r--r--odoo/tests/common.py61
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&amp;model=digest.digest&amp;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">&lt;!DOCTYPE html&gt;
<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">&amp;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