summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-23 17:40:06 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-24 09:44:56 +0000
commitac4fc6e7c7d71c3f8e2440a8ed8095c5aab15dd8 (patch)
treee143d6d261f22cb06f2c0d5b344a4353e5b3be1b
parenta6a3df3d0eda0fc2476c59de61d59534815f7c20 (diff)
parentbd3e510edbcd62f508dae28f573ee4d1a51bcf87 (diff)
[MERGE] forward port branch saas-12.3 up to bd3e510edbc
closes odoo/odoo#37314 Signed-off-by: Christophe Simonis <chs@odoo.com>
-rw-r--r--addons/account/i18n/account.pot6
-rw-r--r--addons/account/models/account_move.py12
-rw-r--r--addons/account/models/account_reconcile_model.py8
-rw-r--r--addons/account/models/reconciliation_widget.py11
-rw-r--r--addons/account/models/res_users.py8
-rw-r--r--addons/account/tests/test_reconciliation_widget.py30
-rw-r--r--addons/account/views/account_view.xml2
-rw-r--r--addons/auth_signup/models/res_partner.py2
-rw-r--r--addons/digest/data/res_config_settings_data.xml6
-rw-r--r--addons/event/controllers/main.py2
-rw-r--r--addons/l10n_fr/data/account_fiscal_position_template_data.xml80
-rw-r--r--addons/l10n_fr/data/account_tax_data.xml415
-rw-r--r--addons/l10n_in/data/account_data.xml6
-rw-r--r--addons/l10n_in/data/account_tax_template_data.xml755
-rw-r--r--addons/l10n_it_edi/models/account_invoice.py5
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py14
-rw-r--r--addons/mail/static/src/js/discuss.js134
-rw-r--r--addons/mail/static/src/js/services/mail_document_thread_manager.js2
-rw-r--r--addons/mail/static/src/js/services/mail_manager.js7
-rw-r--r--addons/mail/static/src/js/systray/systray_activity_menu.js3
-rw-r--r--addons/mail/static/src/js/systray/systray_messaging_menu.js25
-rw-r--r--addons/mail/static/src/js/thread_field.js26
-rw-r--r--addons/mail/static/src/scss/discuss.scss12
-rw-r--r--addons/mail/static/src/xml/discuss.xml9
-rw-r--r--addons/mail/static/src/xml/systray.xml2
-rw-r--r--addons/mail/static/tests/discuss_tests.js50
-rw-r--r--addons/mail/static/tests/systray/systray_messaging_menu_tests.js47
-rw-r--r--addons/mrp/models/mrp_production.py1
-rw-r--r--addons/mrp/models/product.py5
-rw-r--r--addons/payment/models/payment_acquirer.py14
-rw-r--r--addons/payment_stripe_sca/models/payment.py41
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/product/models/product_pricelist.py6
-rw-r--r--addons/product/models/product_template.py2
-rw-r--r--addons/snailmail/models/res_partner.py19
-rw-r--r--addons/stock/models/stock_move.py6
-rw-r--r--addons/stock/models/stock_picking.py36
-rw-r--r--addons/stock/tests/test_move2.py14
-rw-r--r--addons/survey/controllers/main.py16
-rw-r--r--addons/survey/static/src/js/survey.js10
-rw-r--r--addons/web/static/src/css/reset.min.css4
-rw-r--r--addons/web/static/src/js/core/dom.js2
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js6
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js5
-rw-r--r--addons/web/static/src/js/fields/field_utils.js4
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js92
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js160
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js72
-rw-r--r--addons/web/static/src/js/views/list/list_model.js10
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js21
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js5
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js9
-rw-r--r--addons/web/static/src/xml/base.xml4
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js132
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js34
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2one_tests.js75
-rw-r--r--addons/web/static/tests/views/list_tests.js398
-rw-r--r--addons/web/static/tests/views/search_view_tests.js1
-rw-r--r--addons/web/static/tests/widgets/domain_selector_tests.js1
-rw-r--r--addons/website/static/src/scss/website.scss8
-rw-r--r--addons/website_event_questions/report/report_event_question_view.xml3
-rw-r--r--addons/website_sale/data/mail_template_data.xml2
-rw-r--r--addons/website_sale_wishlist/controllers/main.py2
-rw-r--r--doc/howtos/website/module-empty2
-rw-r--r--doc/reference/actions.rst313
-rw-r--r--doc/reference/cmdline.rst333
-rw-r--r--doc/reference/data.rst55
-rw-r--r--doc/reference/javascript_api.rst5
-rw-r--r--doc/reference/javascript_cheatsheet.rst6
-rw-r--r--doc/reference/javascript_reference.rst24
-rw-r--r--doc/reference/mixins.rst155
-rw-r--r--doc/reference/module.rst4
-rw-r--r--doc/reference/qweb.rst26
-rw-r--r--doc/reference/reports.rst16
-rw-r--r--doc/reference/test-report.txt25
-rw-r--r--doc/reference/testing.rst15
-rw-r--r--doc/reference/translations.rst11
-rw-r--r--doc/setup/deploy.rst12
-rw-r--r--doc/setup/install.rst555
-rw-r--r--doc/webservices/iap.rst30
-rw-r--r--doc/webservices/odoo.rst78
-rw-r--r--doc/webservices/upgrade.rst2
-rw-r--r--odoo/addons/base/data/res_currency_data.xml2
-rw-r--r--odoo/addons/base/models/assetsbundle.py16
-rw-r--r--odoo/addons/base/models/ir_actions.py34
-rw-r--r--odoo/addons/base/models/ir_cron.py1
-rw-r--r--odoo/addons/base/models/res_config.py6
-rw-r--r--odoo/addons/base/models/res_users.py11
-rw-r--r--odoo/addons/base/tests/__init__.py1
-rw-r--r--odoo/addons/base/tests/test_non_regression.py32
-rw-r--r--odoo/addons/base/tests/test_user_has_group.py142
-rw-r--r--odoo/service/model.py1
-rw-r--r--odoo/service/server.py4
-rw-r--r--odoo/tools/misc.py23
94 files changed, 3605 insertions, 1236 deletions
diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot
index 43deedf6f9d..1ea910942bf 100644
--- a/addons/account/i18n/account.pot
+++ b/addons/account/i18n/account.pot
@@ -3856,6 +3856,12 @@ msgid ""
msgstr ""
#. module: account
+#: code:addons/account/models/account_move.py:408
+#, python-format
+msgid "Difference debit - credit: "
+msgstr ""
+
+#. module: account
#: model:ir.model,name:account.model_digest_digest
msgid "Digest"
msgstr ""
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 8a6de4e1ccf..b0f559a283b 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1224,10 +1224,15 @@ class AccountMove(models.Model):
lang_env = move.with_context(lang=move.partner_id.lang).env
tax_lines = move.line_ids.filtered(lambda line: line.tax_line_id)
res = {}
+ # There are as many tax line as there are repartition lines
+ done_taxes = set()
for line in tax_lines:
res.setdefault(line.tax_line_id.tax_group_id, {'base': 0.0, 'amount': 0.0})
res[line.tax_line_id.tax_group_id]['amount'] += line.price_subtotal
- res[line.tax_line_id.tax_group_id]['base'] += line.tax_base_amount
+ if line.tax_line_id.id not in done_taxes:
+ # The base should be added ONCE
+ res[line.tax_line_id.tax_group_id]['base'] += line.tax_base_amount
+ done_taxes.add(line.tax_line_id.id)
res = sorted(res.items(), key=lambda l: l[0].sequence)
move.amount_by_group = [(
group.name, amounts['amount'],
@@ -1304,7 +1309,7 @@ class AccountMove(models.Model):
# are already done. Then, this query MUST NOT depend of computed stored fields (e.g. balance).
# It happens as the ORM makes the create with the 'no_recompute' statement.
self._cr.execute('''
- SELECT line.move_id
+ SELECT line.move_id, ROUND(SUM(debit - credit), currency.decimal_places)
FROM account_move_line line
JOIN account_move move ON move.id = line.move_id
JOIN account_journal journal ON journal.id = move.journal_id
@@ -1318,7 +1323,8 @@ class AccountMove(models.Model):
query_res = self._cr.fetchall()
if query_res:
ids = [res[0] for res in query_res]
- raise UserError(_("Cannot create unbalanced journal entry. Ids: %s") % str(ids))
+ sums = [res[1] for res in query_res]
+ raise UserError(_("Cannot create unbalanced journal entry. Ids: %s\nDifferences debit - credit: %s") % (ids, sums))
@api.multi
def _check_fiscalyear_lock_date(self):
diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py
index 6c7b28d89c7..43651c74bcd 100644
--- a/addons/account/models/account_reconcile_model.py
+++ b/addons/account/models/account_reconcile_model.py
@@ -229,6 +229,7 @@ class AccountReconcileModel(models.Model):
'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
'debit': line_balance > 0 and line_balance or 0,
'credit': line_balance < 0 and -line_balance or 0,
+ 'reconcile_model_id': self.id,
}
new_aml_dicts.append(writeoff_line)
@@ -243,7 +244,11 @@ class AccountReconcileModel(models.Model):
# Second write-off line.
if self.has_second_line and self.second_account_id:
- line_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)
+ remaining_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)
+ if self.second_amount_type == 'percentage':
+ line_balance = remaining_balance * (self.second_amount / 100.0)
+ else:
+ line_balance = self.second_amount * (1 if remaining_balance > 0.0 else -1)
second_writeoff_line = {
'name': self.second_label or st_line.name,
'account_id': self.second_account_id.id,
@@ -251,6 +256,7 @@ class AccountReconcileModel(models.Model):
'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)],
'debit': line_balance > 0 and line_balance or 0,
'credit': line_balance < 0 and -line_balance or 0,
+ 'reconcile_model_id': self.id,
}
new_aml_dicts.append(second_writeoff_line)
diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py
index 85a755c2920..0544a413acd 100644
--- a/addons/account/models/reconciliation_widget.py
+++ b/addons/account/models/reconciliation_widget.py
@@ -522,22 +522,16 @@ class AccountReconciliation(models.AbstractModel):
])
domain = expression.OR([domain_reconciliation, domain_matching])
- partner_domain = []
if partner_id:
- partner_domain = [('partner_id', '=', partner_id)]
- domain = expression.AND([domain, partner_domain])
+ domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
# Domain factorized for all reconciliation use cases
if search_str:
str_domain = self._domain_move_lines(search_str=search_str)
- if not partner_id:
- partner_domain = [('partner_id.name', 'ilike', search_str)]
-
str_domain = expression.OR([
str_domain,
- partner_domain,
+ [('partner_id.name', 'ilike', search_str)]
])
-
domain = expression.AND([
domain,
str_domain
@@ -553,7 +547,6 @@ class AccountReconciliation(models.AbstractModel):
if st_line.company_id.account_bank_reconciliation_start:
domain = expression.AND([domain, [('date', '>=', st_line.company_id.account_bank_reconciliation_start)]])
-
return domain
@api.model
diff --git a/addons/account/models/res_users.py b/addons/account/models/res_users.py
index 013acde54ff..cc423c2d3a5 100644
--- a/addons/account/models/res_users.py
+++ b/addons/account/models/res_users.py
@@ -13,8 +13,12 @@ class Users(models.Model):
def _check_one_user_type(self):
super(Users, self)._check_one_user_type()
- g1 = self.env.ref('account.group_show_line_subtotals_tax_included')
- g2 = self.env.ref('account.group_show_line_subtotals_tax_excluded')
+ g1 = self.env.ref('account.group_show_line_subtotals_tax_included', False)
+ g2 = self.env.ref('account.group_show_line_subtotals_tax_excluded', False)
+
+ if not g1 or not g2:
+ # A user cannot be in a non-existant group
+ return
if self._has_multiple_groups([g1.id, g2.id]):
raise ValidationError(_("A user cannot have both Tax B2B and Tax B2C.\n"
diff --git a/addons/account/tests/test_reconciliation_widget.py b/addons/account/tests/test_reconciliation_widget.py
index a588fccd42c..ebd23ddd434 100644
--- a/addons/account/tests/test_reconciliation_widget.py
+++ b/addons/account/tests/test_reconciliation_widget.py
@@ -57,6 +57,7 @@ class TestReconciliationWidget(TestReconciliation):
def test_filter_partner1(self):
inv1 = self.create_invoice(currency_id=self.currency_euro_id)
inv2 = self.create_invoice(currency_id=self.currency_euro_id)
+ partner = inv1.partner_id
receivable1 = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
receivable2 = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
@@ -75,7 +76,31 @@ class TestReconciliationWidget(TestReconciliation):
'date': time.strftime('%Y-07-15'),
})
- # This is like opening the widget, and type "deco" in the filter
+ # This is like input a partner in the widget
+ mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
+ bank_stmt_line.id,
+ partner_id=partner.id,
+ excluded_ids=[],
+ search_str=False
+ )
+ mv_lines_ids = [l['id'] for l in mv_lines_rec]
+
+ self.assertTrue(receivable1.id in mv_lines_ids)
+ self.assertTrue(receivable2.id in mv_lines_ids)
+
+ # With a partner set, type the invoice reference in the filter
+ mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
+ bank_stmt_line.id,
+ partner_id=partner.id,
+ excluded_ids=[],
+ search_str=inv1.invoice_payment_ref,
+ )
+ mv_lines_ids = [l['id'] for l in mv_lines_rec]
+
+ self.assertTrue(receivable1.id in mv_lines_ids)
+ self.assertFalse(receivable2.id in mv_lines_ids)
+
+ # Without a partner set, type "deco" in the filter
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
bank_stmt_line.id,
partner_id=False,
@@ -87,8 +112,7 @@ class TestReconciliationWidget(TestReconciliation):
self.assertTrue(receivable1.id in mv_lines_ids)
self.assertTrue(receivable2.id in mv_lines_ids)
- # This is like clicking on the first receivable
- partner = inv1.partner_id
+ # With a partner set, type "deco" in the filter and click on the first receivable
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
bank_stmt_line.id,
partner_id=partner.id,
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index c70b70e7dc9..c709897ca69 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -1128,7 +1128,7 @@ action = model.setting_init_bank_account_action()
<field name="include_base_amount" attrs="{'invisible':[('amount_type','=', 'group')]}" />
<field name="hide_tax_exigibility" invisible="1"/>
<field name="tax_exigibility" widget="radio" attrs="{'invisible':['|', ('amount_type','=', 'group'), ('hide_tax_exigibility', '=', False)]}" groups="account.group_account_user"/>
- <field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_user"/>
+ <field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')]}" groups="account.group_account_user"/>
<field name="cash_basis_transition_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_user"/>
</group>
</group>
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index c041ffd3ccf..6f484117fa0 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -113,7 +113,7 @@ class ResPartner(models.Model):
allow_signup = self.env['res.users']._get_signup_invitation_scope() == 'b2c'
for partner in self:
- if allow_signup and not partner.user_ids:
+ if allow_signup and not partner.sudo().user_ids:
partner = partner.sudo()
partner.signup_prepare()
res[partner.id]['auth_signup_token'] = partner.signup_token
diff --git a/addons/digest/data/res_config_settings_data.xml b/addons/digest/data/res_config_settings_data.xml
index cbd93e4fef0..cacc318daf0 100644
--- a/addons/digest/data/res_config_settings_data.xml
+++ b/addons/digest/data/res_config_settings_data.xml
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
-<odoo>
- <record id="default_emails_digest" model="ir.config_parameter">
+<odoo noupdate="1">
+ <record id="default_emails_digest" model="ir.config_parameter" forcecreate="0">
<field name="key">digest.default_digest_emails</field>
<field name="value">True</field>
</record>
- <record id="default_digest" model="ir.config_parameter">
+ <record id="default_digest" model="ir.config_parameter" forcecreate="0">
<field name="key">digest.default_digest_id</field>
<field name="value" ref="digest.digest_digest_default"/>
</record>
diff --git a/addons/event/controllers/main.py b/addons/event/controllers/main.py
index f844e6a5d51..723d30aaa51 100644
--- a/addons/event/controllers/main.py
+++ b/addons/event/controllers/main.py
@@ -10,8 +10,6 @@ class EventController(Controller):
@route(['''/event/<model("event.event", "[('state', 'in', ('confirm', 'done'))]"):event>/ics'''], type='http', auth="public")
def event_ics_file(self, event, **kwargs):
- if not event or not event.registration_ids:
- return request.not_found()
files = event._get_ics_file()
if not event.id in files:
return NotFound()
diff --git a/addons/l10n_fr/data/account_fiscal_position_template_data.xml b/addons/l10n_fr/data/account_fiscal_position_template_data.xml
index c1b3663520f..c82d89442e7 100644
--- a/addons/l10n_fr/data/account_fiscal_position_template_data.xml
+++ b/addons/l10n_fr/data/account_fiscal_position_template_data.xml
@@ -54,6 +54,11 @@
<field name="tax_src_id" ref="tva_normale_ttc" />
<field name="tax_dest_id" ref="tva_intra_0" />
</record>
+ <record id="fp_tax_template_intraeub2b_vt_intermediaire_encaissement" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_intermediaire_encaissement" />
+ <field name="tax_dest_id" ref="tva_intra_0" />
+ </record>
<record id="fp_tax_template_intraeub2b_vt_normale_encaissement" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
<field name="tax_src_id" ref="tva_normale_encaissement" />
@@ -64,6 +69,11 @@
<field name="tax_src_id" ref="tva_normale_encaissement_ttc" />
<field name="tax_dest_id" ref="tva_intra_0" />
</record>
+ <record id="fp_tax_template_intraeub2b_vt_intermediaire_encaissement_ttc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_intermediaire_encaissement_ttc" />
+ <field name="tax_dest_id" ref="tva_intra_0" />
+ </record>
<!-- Taux DOM-TOM -->
<record id="fp_tax_template_intraeub2b_vt_specifique" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
@@ -146,11 +156,31 @@
<field name="tax_src_id" ref="tva_acq_specifique" />
<field name="tax_dest_id" ref="tva_intra_specifique" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_deduc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement" />
+ <field name="tax_dest_id" ref="tva_intra_normale" />
+ </record>
<record id="fp_tax_template_intraeub2b_ha_specifique_acq" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
<field name="tax_src_id" ref="tva_acq_specifique" />
<field name="tax_dest_id" ref="tva_acq_intra_specifique" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_acq" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement" />
+ <field name="tax_dest_id" ref="tva_acq_intra_normale" />
+ </record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_deduc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
+ <field name="tax_dest_id" ref="tva_intra_intermediaire" />
+ </record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_acq" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
+ <field name="tax_dest_id" ref="tva_acq_intra_intermediaire" />
+ </record>
<!-- Taux Intermédiaire -->
<record id="fp_tax_template_intraeub2b_ha_intermediaire_deduc" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
@@ -168,22 +198,42 @@
<field name="tax_src_id" ref="tva_acq_reduite" />
<field name="tax_dest_id" ref="tva_intra_reduite" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_reduite_deduc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
+ <field name="tax_dest_id" ref="tva_intra_reduite" />
+ </record>
<record id="fp_tax_template_intraeub2b_ha_reduite_acq" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
<field name="tax_src_id" ref="tva_acq_reduite" />
<field name="tax_dest_id" ref="tva_acq_intra_reduite" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_reduite_acq" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
+ <field name="tax_dest_id" ref="tva_acq_intra_reduite" />
+ </record>
<!-- Taux super réduit -->
<record id="fp_tax_template_intraeub2b_ha_super_reduite_deduc" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
<field name="tax_src_id" ref="tva_acq_super_reduite" />
<field name="tax_dest_id" ref="tva_intra_super_reduite" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_super_reduite_deduc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
+ <field name="tax_dest_id" ref="tva_intra_super_reduite" />
+ </record>
<record id="fp_tax_template_intraeub2b_ha_super_reduite_acq" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
<field name="tax_src_id" ref="tva_acq_super_reduite" />
<field name="tax_dest_id" ref="tva_acq_intra_super_reduite" />
</record>
+ <record id="fp_tax_template_intraeub2b_ha_encaissement_super_reduite_acq" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_intraeub2b" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
+ <field name="tax_dest_id" ref="tva_acq_intra_super_reduite" />
+ </record>
<!-- Import/Export + DOM/TOM -->
<!-- ventes -->
@@ -203,11 +253,21 @@
<field name="tax_src_id" ref="tva_normale_encaissement" />
<field name="tax_dest_id" ref="tva_export_0" />
</record>
+ <record id="fp_tax_template_impexp_vt_intermediaire_encaissement" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_intermediaire_encaissement" />
+ <field name="tax_dest_id" ref="tva_export_0" />
+ </record>
<record id="fp_tax_template_impexp_vt_normale_encaissement_ttc" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_import_export" />
<field name="tax_src_id" ref="tva_normale_encaissement_ttc" />
<field name="tax_dest_id" ref="tva_export_0" />
</record>
+ <record id="fp_tax_template_impexp_vt_intermediaire_encaissement_ttc" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_intermediaire_encaissement_ttc" />
+ <field name="tax_dest_id" ref="tva_export_0" />
+ </record>
<!-- Taux DOM-TOM -->
<record id="fp_tax_template_impexp_vt_specifique" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_import_export" />
@@ -280,6 +340,11 @@
<field name="tax_dest_id" ref="tva_import_0" />
</record>
<!-- Taux DOM-TOM -->
+ <record id="fp_tax_template_impexp_ha_encaissement" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_acq_encaissement" />
+ <field name="tax_dest_id" ref="tva_import_0" />
+ </record>
<record id="fp_tax_template_impexp_ha_specifique" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_import_export" />
<field name="tax_src_id" ref="tva_acq_specifique" />
@@ -291,12 +356,27 @@
<field name="tax_src_id" ref="tva_acq_intermediaire" />
<field name="tax_dest_id" ref="tva_import_0" />
</record>
+ <record id="fp_tax_template_impexp_ha_intermediaire" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
+ <field name="tax_dest_id" ref="tva_import_0" />
+ </record>
<!-- Taux Réduit -->
<record id="fp_tax_template_impexp_ha_reduite" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_import_export" />
<field name="tax_src_id" ref="tva_acq_reduite" />
<field name="tax_dest_id" ref="tva_import_0" />
</record>
+ <record id="fp_tax_template_impexp_ha_encaissement_super_reduite" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
+ <field name="tax_dest_id" ref="tva_import_0" />
+ </record>
+ <record id="fp_tax_template_impexp_ha_encaissement_reduite" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fiscal_position_template_import_export" />
+ <field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
+ <field name="tax_dest_id" ref="tva_import_0" />
+ </record>
<!-- Taux super réduit -->
<record id="fp_tax_template_impexp_ha_super_reduite" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_import_export" />
diff --git a/addons/l10n_fr/data/account_tax_data.xml b/addons/l10n_fr/data/account_tax_data.xml
index 3f72c43b63e..41fc79e15a1 100644
--- a/addons/l10n_fr/data/account_tax_data.xml
+++ b/addons/l10n_fr/data/account_tax_data.xml
@@ -46,6 +46,47 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_intermediaire_encaissement">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (vente) 10,0%</field>
+ <field name="description">TVA 10%</field>
+ <field name="amount" eval="10.0"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_445712" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">sale</field>
+ <field name="tax_group_id" ref="tax_group_tva_10"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445712'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_normale_encaissement">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA à l'encaissement (vente) 20,0%</field>
@@ -367,6 +408,48 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_intermediaire_encaissement_ttc">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (vente) 10,0% TTC</field>
+ <field name="description">TVA 10% TTC</field>
+ <field name="price_include" eval="1"/>
+ <field name="amount" eval="10"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_445712" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">sale</field>
+ <field name="tax_group_id" ref="tax_group_tva_10"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445712'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_normale_encaissement_ttc">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA à l'encaissement (vente) 20,0% TTC</field>
@@ -693,6 +776,88 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_encaissement">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 20,0%</field>
+ <field name="description">TVA 20%</field>
+ <field name="amount" eval="20.0"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_20"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
+ <record model="account.tax.template" id="tva_acq_intermediaire_encaissement">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 10,0%</field>
+ <field name="description">TVA 10%</field>
+ <field name="amount" eval="10.0"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_10"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_specifique">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 8,5%</field>
@@ -810,6 +975,47 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_encaissement_reduite">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 5,5%</field>
+ <field name="description">TVA 5,5%</field>
+ <field name="amount" eval="5.5"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_55"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_super_reduite">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 2,1%</field>
@@ -851,6 +1057,47 @@
<!-- ACHATS Tax included in price -->
+ <record model="account.tax.template" id="tva_acq_encaissement_super_reduite">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 2,1% </field>
+ <field name="description">TVA 2,1%</field>
+ <field name="amount" eval="2.1"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_21"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_normale_TTC">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 20,0% TTC</field>
@@ -891,6 +1138,48 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_encaissement_TTC">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 20,0% TTC</field>
+ <field name="description">TVA 20% TTC</field>
+ <field name="price_include" eval="1"/>
+ <field name="amount" eval="20.0"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_20"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_specifique_TTC">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 8,5% TTC</field>
@@ -931,6 +1220,48 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_intermediaire_encaissement_TTC">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 10,0% TTC</field>
+ <field name="description">TVA 10% TTC</field>
+ <field name="price_include" eval="1"/>
+ <field name="amount" eval="10.0"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_10"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_intermediaire_TTC">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 10,0% TTC</field>
@@ -1011,6 +1342,48 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_encaissement_reduite_TTC">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 5,5% TTC</field>
+ <field name="description">TVA 5,5% TTC</field>
+ <field name="price_include" eval="1"/>
+ <field name="amount" eval="5.5"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_55"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<record model="account.tax.template" id="tva_acq_super_reduite_TTC">
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
<field name="name">TVA déductible (achat) 2,1% TTC</field>
@@ -1051,6 +1424,48 @@
]"/>
</record>
+ <record model="account.tax.template" id="tva_acq_encaissement_super_reduite_TTC">
+ <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
+ <field name="name">TVA à l'encaissement (achat) 2,1% TTC</field>
+ <field name="description">TVA 2,1% TTC</field>
+ <field name="price_include" eval="1"/>
+ <field name="amount" eval="2.1"/>
+ <field name="amount_type">percent</field>
+ <field name="tax_exigibility">on_payment</field>
+ <field name="cash_basis_transition_account_id" ref="pcg_44566" />
+ <field name="sequence" eval="10"/>
+ <field name="type_tax_use">purchase</field>
+ <field name="tax_group_id" ref="tax_group_tva_21"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
+ 'account_id': ref('pcg_445800'),
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
+ 'account_id': ref('pcg_44566'),
+ }),
+ ]"/>
+ </record>
+
<!-- ImmoBILISATIONS (achats) -->
diff --git a/addons/l10n_in/data/account_data.xml b/addons/l10n_in/data/account_data.xml
index c1fb9299c1e..c20cfa8839f 100644
--- a/addons/l10n_in/data/account_data.xml
+++ b/addons/l10n_in/data/account_data.xml
@@ -3,6 +3,12 @@
<data noupdate="1">
<!-- Account Tax Group -->
+ <record id="sgst_group" model="account.tax.group">
+ <field name="name">SGST</field>
+ </record>
+ <record id="cgst_group" model="account.tax.group">
+ <field name="name">CGST</field>
+ </record>
<record id="igst_group" model="account.tax.group">
<field name="name">IGST</field>
</record>
diff --git a/addons/l10n_in/data/account_tax_template_data.xml b/addons/l10n_in/data/account_tax_template_data.xml
index 6c0d3b8892e..374628bb05d 100644
--- a/addons/l10n_in/data/account_tax_template_data.xml
+++ b/addons/l10n_in/data/account_tax_template_data.xml
@@ -459,14 +459,14 @@ if tax > result:result=tax</field>
<!-- SGST & CGST Sales Group Tax -->
- <record id="sgst_sale_1" model="account.tax.template">
- <field name="name">GST 1%</field>
- <field name="description">GST 1%</field>
- <field name="type_tax_use">sale</field>
+ <record id="sgst_sale_0_5" model="account.tax.template">
+ <field name="name">SGST Sale 0.5%</field>
+ <field name="description">SGST 0.5%</field>
+ <field name="type_tax_use">none</field>
<field name="amount_type">percent</field>
- <field name="amount">1.0</field>
+ <field name="amount">0.5</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
- <field name="tax_group_id" ref="gst_group"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -474,17 +474,92 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
}),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10051'),
+ 'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_sale_0_5" model="account.tax.template">
+ <field name="name">CGST Sale 0.5%</field>
+ <field name="description">CGST 0.5%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">0.5</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10052'),
+ 'minus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="sgst_sale_1" model="account.tax.template">
+ <field name="name">GST 1%</field>
+ <field name="description">GST 1%</field>
+ <field name="type_tax_use">sale</field>
+ <field name="amount_type">group</field>
+ <field name="amount">1.0</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_0_5'), ref('cgst_sale_0_5'),])]"/>
+ </record>
+
+ <record id="sgst_sale_1_2" model="account.tax.template">
+ <field name="name">SGST Sale 1%</field>
+ <field name="description">SGST 1%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">1</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11232'),
+ 'plus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -493,17 +568,45 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_sale_1_2" model="account.tax.template">
+ <field name="name">CGST Sale 1%</field>
+ <field name="description">CGST 1%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">1</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11233'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -511,10 +614,21 @@ if tax > result:result=tax</field>
<field name="name">GST 2%</field>
<field name="description">GST 2%</field>
<field name="type_tax_use">sale</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">2.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_1_2'), ref('cgst_sale_1_2'),])]"/>
+ </record>
+
+ <record id="sgst_sale_14" model="account.tax.template">
+ <field name="name">SGST Sale 14%</field>
+ <field name="description">SGST 14%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">14</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -522,17 +636,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11233'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -541,17 +649,45 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_sale_14" model="account.tax.template">
+ <field name="name">CGST Sale 14%</field>
+ <field name="description">CGST 14%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">14</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11233'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -559,10 +695,21 @@ if tax > result:result=tax</field>
<field name="name">GST 28%</field>
<field name="description">GST 28%</field>
<field name="type_tax_use">sale</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">28.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_14'),ref('cgst_sale_14'),])]"/>
+ </record>
+
+ <record id="sgst_sale_9" model="account.tax.template">
+ <field name="name">SGST Sale 9%</field>
+ <field name="description">SGST 9%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">9</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -570,17 +717,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11233'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -589,17 +730,45 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_sale_9" model="account.tax.template">
+ <field name="name">CGST Sale 9%</field>
+ <field name="description">CGST 9%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">9</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11233'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -607,43 +776,74 @@ if tax > result:result=tax</field>
<field name="name">GST 18%</field>
<field name="description">GST 18%</field>
<field name="type_tax_use">sale</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">18.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_9'),ref('cgst_sale_9'),])]"/>
+ </record>
+
+ <record id="sgst_sale_6" model="account.tax.template">
+ <field name="name">SGST Sale 6%</field>
+ <field name="description">SGST 6%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">6</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11233'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_sale_6" model="account.tax.template">
+ <field name="name">CGST Sale 6%</field>
+ <field name="description">CGST 6%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">6</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11233'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
@@ -655,10 +855,21 @@ if tax > result:result=tax</field>
<field name="name">GST 12%</field>
<field name="description">GST 12%</field>
<field name="type_tax_use">sale</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">12.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_6'),ref('cgst_sale_6'),])]"/>
+ </record>
+
+ <record id="sgst_sale_2_5" model="account.tax.template">
+ <field name="name">SGST Sale 2.5%</field>
+ <field name="description">SGST 2.5%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">2.5</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -666,17 +877,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11233'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -685,69 +890,60 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10052'),
- 'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
- <record id="sgst_sale_5" model="account.tax.template">
- <field name="name">GST 5%</field>
- <field name="description">GST 5%</field>
- <field name="type_tax_use">sale</field>
+ <record id="cgst_sale_2_5" model="account.tax.template">
+ <field name="name">CGST Sale 2.5%</field>
+ <field name="description">CGST 2.5%</field>
+ <field name="type_tax_use">none</field>
<field name="amount_type">percent</field>
- <field name="amount">5.0</field>
+ <field name="amount">2.5</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
- <field name="tax_group_id" ref="gst_group"/>
- <field name="sequence">0</field>
+ <field name="tax_group_id" ref="cgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11232'),
- 'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10051'),
- 'minus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
+ <record id="sgst_sale_5" model="account.tax.template">
+ <field name="name">GST 5%</field>
+ <field name="description">GST 5%</field>
+ <field name="type_tax_use">sale</field>
+ <field name="amount_type">group</field>
+ <field name="amount">5.0</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="gst_group"/>
+ <field name="sequence">0</field>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_sale_2_5'), ref('cgst_sale_2_5'),])]"/>
+ </record>
+
<!-- Purchase Taxes-->
<!-- CESS Taxes -->
@@ -1166,97 +1362,232 @@ if tax > result:result=tax</field>
<!-- SGST & CGST -->
- <record id="sgst_purchase_1" model="account.tax.template">
- <field name="name">GST 1%</field>
- <field name="description">GST 1%</field>
- <field name="type_tax_use">purchase</field>
+ <record id="sgst_purchase_0_5" model="account.tax.template">
+ <field name="name">SGST Purchase 0.5%</field>
+ <field name="description">SGST 0.5%</field>
+ <field name="type_tax_use">none</field>
<field name="amount_type">percent</field>
- <field name="amount">1.0</field>
+ <field name="amount">0.5</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
- <field name="tax_group_id" ref="gst_group"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11232'),
+ 'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_purchase_0_5" model="account.tax.template">
+ <field name="name">CGST Purchase 0.5%</field>
+ <field name="description">CGST 0.5%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">0.5</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p11233'),
+ 'minus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="sgst_purchase_1" model="account.tax.template">
+ <field name="name">GST 1%</field>
+ <field name="description">GST 1%</field>
+ <field name="type_tax_use">purchase</field>
+ <field name="amount_type">group</field>
+ <field name="amount">1.0</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_0_5'),ref('cgst_purchase_0_5'),])]"/>
+ </record>
+
+ <record id="sgst_purchase_1_2" model="account.tax.template">
+ <field name="name">SGST Purchase 1%</field>
+ <field name="description">SGST 1%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">1</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10051'),
+ 'plus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
+
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_purchase_1_2" model="account.tax.template">
+ <field name="name">CGST Purchase 1%</field>
+ <field name="description">CGST 1%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">1</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10052'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
+
</record>
<record id="sgst_purchase_2" model="account.tax.template">
<field name="name">GST 2%</field>
<field name="description">GST 2%</field>
<field name="type_tax_use">purchase</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">2.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_1_2'),ref('cgst_purchase_1_2'),])]"/>
+ </record>
+
+ <record id="sgst_purchase_14" model="account.tax.template">
+ <field name="name">SGST Purchase 14%</field>
+ <field name="description">SGST 14%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">14</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10052'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_purchase_14" model="account.tax.template">
+ <field name="name">CGST Purchase 14%</field>
+ <field name="description">CGST 14%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">14</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10052'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -1264,10 +1595,21 @@ if tax > result:result=tax</field>
<field name="name">GST 28%</field>
<field name="description">GST 28%</field>
<field name="type_tax_use">purchase</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">28.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_14'),ref('cgst_purchase_14'),])]"/>
+ </record>
+
+ <record id="sgst_purchase_9" model="account.tax.template">
+ <field name="name">SGST Purchase 9%</field>
+ <field name="description">SGST 9%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">9</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -1275,17 +1617,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10052'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -1294,17 +1630,45 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_purchase_9" model="account.tax.template">
+ <field name="name">CGST Purchase 9%</field>
+ <field name="description">CGST 9%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">9</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10052'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ })
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -1312,10 +1676,21 @@ if tax > result:result=tax</field>
<field name="name">GST 18%</field>
<field name="description">GST 18%</field>
<field name="type_tax_use">purchase</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">18.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_9'),ref('cgst_purchase_9'),])]"/>
+ </record>
+
+ <record id="sgst_purchase_6" model="account.tax.template">
+ <field name="name">SGST Purchase 6%</field>
+ <field name="description">SGST 6%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">6</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -1323,17 +1698,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10052'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -1342,17 +1711,45 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
+ })
+ ]"/>
+ </record>
+
+ <record id="cgst_purchase_6" model="account.tax.template">
+ <field name="name">CGST Purchase 6%</field>
+ <field name="description">CGST 6%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">6</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="cgst_group"/>
+ <field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': ref('p10052'),
+ 'plus_report_line_ids': [ref('tax_report_line_cgst')],
+ }),
+ ]"/>
+ <field name="refund_repartition_line_ids" eval="[(5, 0, 0),
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0,0, {
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
@@ -1360,10 +1757,21 @@ if tax > result:result=tax</field>
<field name="name">GST 12%</field>
<field name="description">GST 12%</field>
<field name="type_tax_use">purchase</field>
- <field name="amount_type">percent</field>
+ <field name="amount_type">group</field>
<field name="amount">12.0</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tax_group_id" ref="gst_group"/>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_6'),ref('cgst_purchase_6'),])]"/>
+ </record>
+
+ <record id="sgst_purchase_2_5" model="account.tax.template">
+ <field name="name">SGST Purchase 2.5%</field>
+ <field name="description">SGST 2.5%</field>
+ <field name="type_tax_use">none</field>
+ <field name="amount_type">percent</field>
+ <field name="amount">2.5</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="sgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
@@ -1371,17 +1779,11 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10051'),
'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10052'),
- 'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -1390,67 +1792,58 @@ if tax > result:result=tax</field>
}),
(0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11232'),
'minus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11233'),
- 'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
- <record id="sgst_purchase_5" model="account.tax.template">
- <field name="name">GST 5%</field>
- <field name="description">GST 5%</field>
- <field name="type_tax_use">purchase</field>
+ <record id="cgst_purchase_2_5" model="account.tax.template">
+ <field name="name">CGST Purchase 2.5%</field>
+ <field name="description">CGST 2.5%</field>
+ <field name="type_tax_use">none</field>
<field name="amount_type">percent</field>
- <field name="amount">5.0</field>
+ <field name="amount">2.5</field>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
- <field name="tax_group_id" ref="gst_group"/>
- <field name="sequence">0</field>
+ <field name="tax_group_id" ref="cgst_group"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p10051'),
- 'plus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p10052'),
'plus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
-
(0,0, {
- 'factor_percent': 50,
- 'repartition_type': 'tax',
- 'account_id': ref('p11232'),
- 'minus_report_line_ids': [ref('tax_report_line_sgst')],
- }),
- (0,0, {
- 'factor_percent': 50,
+ 'factor_percent': 100,
'repartition_type': 'tax',
'account_id': ref('p11233'),
'minus_report_line_ids': [ref('tax_report_line_cgst')],
- }),
+ })
]"/>
</record>
+ <record id="sgst_purchase_5" model="account.tax.template">
+ <field name="name">GST 5%</field>
+ <field name="description">GST 5%</field>
+ <field name="type_tax_use">purchase</field>
+ <field name="amount_type">group</field>
+ <field name="amount">5.0</field>
+ <field name="chart_template_id" ref="indian_chart_template_standard"/>
+ <field name="tax_group_id" ref="gst_group"/>
+ <field name="sequence">0</field>
+ <field name="children_tax_ids" eval="[(6, 0, [ref('sgst_purchase_2_5'),ref('cgst_purchase_2_5'),])]"/>
+ </record>
+
</odoo>
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index 3a8d8361b95..77185ea4e3d 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -110,7 +110,7 @@ class AccountMove(models.Model):
raise UserError(_("%s must have a country.") % (seller.display_name))
# <1.4.1>
- if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code != 'IT':
+ if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
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>
@@ -273,11 +273,12 @@ class AccountMove(models.Model):
'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.company.l10n_it_address_recipient_fatturapa),
'author_id': self.env.user.partner_id.id,
'email_from': self.env.company.l10n_it_address_send_fatturapa,
+ 'reply_to': self.env.company.l10n_it_address_send_fatturapa,
'mail_server_id': self.env.company.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_fattura = self.env['mail.mail'].with_context(wo_return_path=True).create({
'mail_message_id': message.id,
'email_to': self.env.company.l10n_it_address_recipient_fatturapa,
})
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index 581274ac404..18c4a66af3c 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -355,3 +355,17 @@ class FetchmailServer(models.Model):
if descrizione:
output_str += "<li>Errore %s: %s</li>" % (element[0].text, descrizione)
return output_str + "</ul>"
+
+class IrMailServer(models.Model):
+ _name = "ir.mail_server"
+ _inherit = "ir.mail_server"
+
+ def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
+ attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
+ body_alternative=None, subtype_alternative='plain'):
+
+ if self.env.context.get('wo_return_path') and headers:
+ headers.pop('Return-Path', False)
+ return super(IrMailServer, self).build_email(email_from, email_to, subject, body, email_cc=email_cc, email_bcc=email_bcc, reply_to=reply_to,
+ attachments=attachments, message_id=message_id, references=references, object_id=object_id, subtype=subtype, headers=headers,
+ body_alternative=body_alternative, subtype_alternative=subtype_alternative)
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 6eeeaecf7c3..de5ce88b742 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -280,6 +280,8 @@ var Discuss = AbstractAction.extend({
this.options.channelQuickSearchThreshold = 20;
}
+ this._isMessagingReady = this.call('mail_service', 'isReady');
+ this._isStarted = false;
this._threadsScrolltop = {};
this._composerStates = {};
this._defaultThreadID = this.options.active_id ||
@@ -291,68 +293,20 @@ var Discuss = AbstractAction.extend({
this._updateThreads.bind(this), 100, { leading: false });
this.controlPanelParams.modelName = 'mail.message';
- },
- /**
- * @override
- */
- willStart: function () {
- return Promise.all([this._super(), this.call('mail_service', 'isReady')]);
+ this.call('mail_service', 'getMailBus').on('messaging_ready', this, this._onMessagingReady);
},
/**
* @override
*/
start: function () {
var self = this;
-
- this._basicComposer = new BasicComposer(this, {
- mentionPartnersRestricted: true,
- showTyping: true,
- });
- this._extendedComposer = new ExtendedComposer(this, {
- mentionPartnersRestricted: true,
- showTyping: true,
+ this._isStarted = true;
+ return this._super.apply(this, arguments).then(function () {
+ if (!self._isMessagingReady) {
+ return;
+ }
+ return self._initRender();
});
- this._basicComposer
- .on('post_message', this, this._onPostMessage)
- .on('input_focused', this, this._onComposerFocused);
- this._extendedComposer
- .on('post_message', this, this._onPostMessage)
- .on('input_focused', this, this._onComposerFocused);
- this._renderButtons();
-
- var defs = [];
- defs.push(
- this._renderThread()
- );
- defs.push(
- this._basicComposer.appendTo(this.$('.o_mail_discuss_content')));
- defs.push(
- this._extendedComposer.appendTo(this.$('.o_mail_discuss_content')));
- defs.push(this._super.apply(this, arguments));
-
- return Promise.all(defs)
- .then(function () {
- return self._setThread(self._defaultThreadID);
- })
- .then(function () {
- self._initThreads();
- self._startListening();
- self._threadWidget.$el.on('scroll', null, _.debounce(function () {
- if (
- self._threadWidget.getScrolltop() < 20 &&
- !self._threadWidget.$('.o_mail_no_content').length &&
- !self._thread.isAllHistoryLoaded(self.domain)
- ) {
- self._loadMoreMessages();
- }
- if (
- self._threadWidget.isAtBottom() &&
- self._thread.getType() !== 'mailbox'
- ) {
- self._thread.markAsRead();
- }
- }, 100));
- });
},
/**
* @override
@@ -381,8 +335,8 @@ var Discuss = AbstractAction.extend({
this.call('mail_service', 'getMailBus').trigger('discuss_open', true);
if (this._thread) {
this._threadWidget.scrollToPosition(this._threadsScrolltop[this._thread.getID()]);
+ this._loadEnoughMessages();
}
- this._loadEnoughMessages();
},
/**
* @override
@@ -547,6 +501,60 @@ var Discuss = AbstractAction.extend({
}
},
/**
+ * @private
+ */
+ _initRender: function () {
+ var self = this;
+ this._basicComposer = new BasicComposer(this, {
+ mentionPartnersRestricted: true,
+ showTyping: true,
+ });
+ this._extendedComposer = new ExtendedComposer(this, {
+ mentionPartnersRestricted: true,
+ showTyping: true,
+ });
+ this._basicComposer
+ .on('post_message', this, this._onPostMessage)
+ .on('input_focused', this, this._onComposerFocused);
+ this._extendedComposer
+ .on('post_message', this, this._onPostMessage)
+ .on('input_focused', this, this._onComposerFocused);
+ this._renderButtons();
+
+ var defs = [];
+ defs.push(
+ this._renderThread()
+ );
+ defs.push(
+ this._basicComposer.appendTo(this.$('.o_mail_discuss_content')));
+ defs.push(
+ this._extendedComposer.appendTo(this.$('.o_mail_discuss_content')));
+
+ return Promise.all(defs)
+ .then(function () {
+ return self._setThread(self._defaultThreadID);
+ })
+ .then(function () {
+ self._initThreads();
+ self._startListening();
+ self._threadWidget.$el.on('scroll', null, _.debounce(function () {
+ if (
+ self._threadWidget.getScrolltop() < 20 &&
+ !self._threadWidget.$('.o_mail_no_content').length &&
+ !self._thread.isAllHistoryLoaded(self.domain)
+ ) {
+ self._loadMoreMessages();
+ }
+ if (
+ self._threadWidget.isAtBottom() &&
+ self._thread.getType() !== 'mailbox'
+ ) {
+ self._thread.markAsRead();
+ }
+ }, 100));
+ });
+ },
+ /**
* Renders the mainside bar with current threads
*
* @private
@@ -1402,6 +1410,20 @@ var Discuss = AbstractAction.extend({
},
/**
* @private
+ */
+ _onMessagingReady: function () {
+ if (this._isMessagingReady) {
+ return;
+ }
+ this._isMessagingReady = true;
+ if (!this._isStarted) {
+ return;
+ }
+ this.$('.o_mail_discuss_loading').remove();
+ this._initRender();
+ },
+ /**
+ * @private
* @param {MouseEvent} ev
*/
_onModerateAllClicked: function (ev) {
diff --git a/addons/mail/static/src/js/services/mail_document_thread_manager.js b/addons/mail/static/src/js/services/mail_document_thread_manager.js
index 6bb66b1b439..652567baf14 100644
--- a/addons/mail/static/src/js/services/mail_document_thread_manager.js
+++ b/addons/mail/static/src/js/services/mail_document_thread_manager.js
@@ -34,7 +34,7 @@ MailManager.include({
this.call('local_storage', 'getItem', key);
}
}
- this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
+ this._mailBus.on('messaging_ready', this, this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
// listen to localStorage changes to synchronize DocumentThread's
// windows between tabs
this.call('local_storage', 'onStorage', this, this._onStorage);
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 3ed4031abeb..950ac8c5cd6 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -46,6 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers
var MailManager = AbstractService.extend({
dependencies: ['ajax', 'bus_service', 'local_storage'],
+ _isReady: false,
_ODOOBOT_ID: ["ODOOBOT", "ODOOBOT"], // authorID for transient messages
/**
@@ -294,7 +295,7 @@ var MailManager = AbstractService.extend({
* This is the case when it has fetched the initial state from the server,
* by means of the route 'mail/init_messaging'
*
- * @returns {Promise}
+ * @returns {boolean}
*/
isReady: function () {
return this._isReady;
@@ -636,7 +637,7 @@ var MailManager = AbstractService.extend({
*/
_fetchMailStateFromServer: function () {
var self = this;
- this._isReady = session.is_bound.then(function () {
+ session.is_bound.then(function () {
var context = _.extend(
{ isMobile: config.device.isMobile },
session.user_context
@@ -648,6 +649,8 @@ var MailManager = AbstractService.extend({
}).then(function (result) {
self._updateInternalStateFromServer(result);
self.call('bus_service', 'startPolling');
+ self._isReady = true;
+ self._mailBus.trigger('messaging_ready');
});
},
/**
diff --git a/addons/mail/static/src/js/systray/systray_activity_menu.js b/addons/mail/static/src/js/systray/systray_activity_menu.js
index 641ff4be1bc..8afa88d7e20 100644
--- a/addons/mail/static/src/js/systray/systray_activity_menu.js
+++ b/addons/mail/static/src/js/systray/systray_activity_menu.js
@@ -19,9 +19,6 @@ var ActivityMenu = Widget.extend({
'click .o_mail_preview': '_onActivityFilterClick',
'show.bs.dropdown': '_onActivityMenuShow',
},
- willStart: function () {
- return Promise.resolve(this.call('mail_service', 'isReady'));
- },
start: function () {
this._$activitiesPreview = this.$('.o_mail_systray_dropdown_items');
this.call('mail_service', 'getMailBus').on('activity_updated', this, this._updateCounter);
diff --git a/addons/mail/static/src/js/systray/systray_messaging_menu.js b/addons/mail/static/src/js/systray/systray_messaging_menu.js
index f4c4eb48f39..1093a22d27a 100644
--- a/addons/mail/static/src/js/systray/systray_messaging_menu.js
+++ b/addons/mail/static/src/js/systray/systray_messaging_menu.js
@@ -30,18 +30,14 @@ var MessagingMenu = Widget.extend({
/**
* @override
*/
- willStart: function () {
- return Promise.all([this._super.apply(this, arguments), this.call('mail_service', 'isReady')]);
- },
- /**
- * @override
- */
start: function () {
this._$filterButtons = this.$('.o_filter_button');
this._$previews = this.$('.o_mail_systray_dropdown_items');
this._filter = false;
+ this._isMessagingReady = this.call('mail_service', 'isReady');
this._updateCounter();
var mailBus = this.call('mail_service', 'getMailBus');
+ mailBus.on('messaging_ready', this, this._onMessagingReady);
mailBus.on('update_needaction', this, this._updateCounter);
mailBus.on('new_channel', this, this._updateCounter);
mailBus.on('update_thread_unread_counter', this, this._updateCounter);
@@ -207,6 +203,9 @@ var MessagingMenu = Widget.extend({
_updatePreviews: function () {
// Display spinner while waiting for conversations preview
this._$previews.html(QWeb.render('Spinner'));
+ if (!this._isMessagingReady) {
+ return;
+ }
this._getPreviews()
.then(this._renderPreviews.bind(this));
},
@@ -229,7 +228,11 @@ var MessagingMenu = Widget.extend({
* @private
*/
_updateCounter: function () {
+ if (!this._isMessagingReady) {
+ return;
+ }
var counter = this._computeCounter();
+ this.$('.o_mail_messaging_menu_icon').removeClass('fa-spinner fa-spin');
this.$('.o_notification_counter').text(counter);
this.$el.toggleClass('o_no_notification', !counter);
if (this._isShown()) {
@@ -338,6 +341,16 @@ var MessagingMenu = Widget.extend({
}
}
},
+ /**
+ * @private
+ */
+ _onMessagingReady: function () {
+ if (this._isMessagingReady) {
+ return;
+ }
+ this._isMessagingReady = true;
+ this._updateCounter();
+ },
});
// Systray menu items display order matches order in the list
diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js
index ab8023b06ad..c8a59850016 100644
--- a/addons/mail/static/src/js/thread_field.js
+++ b/addons/mail/static/src/js/thread_field.js
@@ -20,6 +20,9 @@ var ThreadField = AbstractField.extend({
*/
init: function () {
this._super.apply(this, arguments);
+ this._isMessagingReady = this.call('mail_service', 'isReady');
+ this._isStarted = false;
+ this.call('mail_service', 'getMailBus').on('messaging_ready', this, this._onMessagingReady);
// Used to automatically mark document thread as read at the moment we
// access the document and render the thread.
this._markAsReadOnNextRender = false;
@@ -28,15 +31,10 @@ var ThreadField = AbstractField.extend({
/**
* @override
*/
- willStart: function () {
- return this.alive(this.call('mail_service', 'isReady'));
- },
- /**
- * @override
- */
start: function () {
var self = this;
+ this._isStarted = true;
this.dp = new concurrency.DropPrevious();
this._threadWidget = new ThreadWidget(this, {
@@ -159,6 +157,9 @@ var ThreadField = AbstractField.extend({
* If it is a new document in create mode, unset the document thread.
*/
_setDocumentThread: function () {
+ if (!this._isMessagingReady) {
+ return;
+ }
var params = {
messageIDs: this.value.res_ids,
name: this.recordData.display_name,
@@ -187,6 +188,19 @@ var ThreadField = AbstractField.extend({
},
/**
* @private
+ */
+ _onMessagingReady: function () {
+ if (this._isMessagingReady) {
+ return;
+ }
+ this._isMessagingReady = true;
+ if (!this._isStarted) {
+ return;
+ }
+ this._setDocumentThread();
+ },
+ /**
+ * @private
* @param {mail.model.Message}
*/
_onNewMessage: function (message) {
diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss
index 8284fdc70b5..4cb902e4539 100644
--- a/addons/mail/static/src/scss/discuss.scss
+++ b/addons/mail/static/src/scss/discuss.scss
@@ -7,6 +7,18 @@
height: 100%;
overflow: hidden;
+ .o_mail_discuss_loading {
+ display: flex;
+ flex: 1 1 auto;
+ align-items: center;
+ justify-content: center;
+ min-height: 100%;
+
+ > i {
+ margin-right: 5px;
+ }
+ }
+
.o_mail_discuss_sidebar {
width: $o-mail-chat-sidebar-width;
height: 100%;
diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml
index afee3a84da2..781a03c016d 100644
--- a/addons/mail/static/src/xml/discuss.xml
+++ b/addons/mail/static/src/xml/discuss.xml
@@ -8,8 +8,15 @@
-->
<t t-name="mail.discuss">
<div class="o_mail_discuss">
- <div class="o_mail_discuss_sidebar"/>
+ <div class="o_mail_discuss_sidebar">
+ <div class="o_mail_discuss_loading">
+ <i class="fa fa-spinner fa-spin"/> Please wait...
+ </div>
+ </div>
<div class="o_mail_discuss_content">
+ <div class="o_mail_discuss_loading">
+ <i class="fa fa-spinner fa-spin"/> Please wait...
+ </div>
</div>
</div>
</t>
diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml
index 7bcda0fb273..204af3700e4 100644
--- a/addons/mail/static/src/xml/systray.xml
+++ b/addons/mail/static/src/xml/systray.xml
@@ -7,7 +7,7 @@
<t t-name="mail.systray.MessagingMenu">
<li class="o_mail_systray_item">
<a class="dropdown-toggle o-no-caret" data-toggle="dropdown" data-display="static" aria-expanded="false" title="Conversations" href="#" role="button">
- <i class="fa fa-comments" role="img" aria-label="Messages"/> <span class="o_notification_counter badge badge-pill"/>
+ <i class="o_mail_messaging_menu_icon fa fa-comments fa-spinner fa-spin" role="img" aria-label="Messages"/> <span class="o_notification_counter badge badge-pill"/>
</a>
<div class="o_mail_systray_dropdown dropdown-menu dropdown-menu-right" role="menu">
<t t-if="widget.isMobile()">
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 226fd1e398a..1119ff2164f 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -114,6 +114,56 @@ QUnit.test('basic rendering', async function (assert) {
discuss.destroy();
});
+QUnit.test('messaging not ready', async function (assert) {
+ assert.expect(9);
+
+ const messagingReadyProm = testUtils.makeTestPromise();
+ const discuss = await createDiscuss({
+ id: 1,
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ async mockRPC(route, args) {
+ if (route === '/mail/init_messaging') {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ assert.step('/mail/init_messaging:pending');
+ await messagingReadyProm;
+ assert.step('/mail/init_messaging:resolved');
+ return _super();
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ assert.verifySteps(['/mail/init_messaging:pending']);
+ assert.ok(
+ discuss.el,
+ "discuss should be rendered");
+ assert.strictEqual(
+ $('.o_action')[0],
+ discuss.el,
+ "should display discuss even when messaging is not ready");
+ assert.containsOnce(
+ discuss,
+ '.o_mail_discuss_sidebar .o_mail_discuss_loading',
+ "should display sidebar is loading (messaging not yet ready)");
+ assert.containsOnce(
+ discuss,
+ '.o_mail_discuss_content .o_mail_discuss_loading',
+ "should display content is loading (messaging not yet ready)");
+
+ messagingReadyProm.resolve();
+ await testUtils.nextTick();
+ assert.verifySteps(['/mail/init_messaging:resolved']);
+ assert.containsNone(
+ discuss,
+ '.o_mail_discuss_loading',
+ "should no longer display sidebar or content is loading (messaging is ready)");
+
+ discuss.destroy();
+});
+
QUnit.test('searchview options visibility', async function (assert) {
assert.expect(5);
diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
index 047d723a425..9281e8a3ba4 100644
--- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
@@ -120,6 +120,53 @@ QUnit.test('messaging menu widget: menu with no records', async function (assert
messagingMenu.destroy();
});
+QUnit.test('messaging menu widget: messaging not ready', async function (assert) {
+ assert.expect(8);
+
+ const messagingReadyProm = testUtils.makeTestPromise();
+
+ const messagingMenu = new MessagingMenu();
+ testUtils.mock.addMockEnvironment(messagingMenu, {
+ services: this.services,
+ async mockRPC(route, args) {
+ if (route === '/mail/init_messaging') {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ assert.step('/mail/init_messaging:pending');
+ await messagingReadyProm;
+ assert.step('/mail/init_messaging:resolved');
+ return _super();
+ }
+ if (args.method === 'message_fetch') {
+ return [];
+ }
+ return this._super(...arguments);
+ },
+ });
+ await messagingMenu.appendTo($('#qunit-fixture'));
+ assert.verifySteps(['/mail/init_messaging:pending']);
+ assert.ok(
+ messagingMenu.el,
+ "messaging menu should be rendered");
+ assert.strictEqual(
+ document.querySelector('.o_mail_systray_item'),
+ messagingMenu.el,
+ "should display messaging menu even when messaging is not ready");
+ assert.hasClass(
+ messagingMenu.$('.o_mail_messaging_menu_icon'),
+ 'fa-spinner',
+ "Messaging menu icon should be a spinner when messaging menu is not ready");
+
+ messagingReadyProm.resolve();
+ await testUtils.nextTick();
+ assert.verifySteps(['/mail/init_messaging:resolved']);
+ assert.doesNotHaveClass(
+ messagingMenu.$('.o_mail_messaging_menu_icon'),
+ 'fa-spinner',
+ "Messaging menu icon should no longer be a spinner when messaging menu is ready");
+
+ messagingMenu.destroy();
+});
+
QUnit.test('messaging menu widget: messaging menu with 1 record', async function (assert) {
assert.expect(3);
var messagingMenu = new MessagingMenu();
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 76e547c6916..0d9db0a5745 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -394,6 +394,7 @@ class MrpProduction(models.Model):
def _onchange_bom_id(self):
self.product_qty = self.bom_id.product_qty
self.product_uom_id = self.bom_id.product_uom_id.id
+ self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id
@api.onchange('date_planned_start')
def _onchange_date_planned_start(self):
diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py
index 7ea6e332123..93f7f49ad6e 100644
--- a/addons/mrp/models/product.py
+++ b/addons/mrp/models/product.py
@@ -94,9 +94,11 @@ class ProductProduct(models.Model):
- 'incoming_qty'
- 'outgoing_qty'
"""
+ kits = self.env['product.product']
for product in self:
bom_kit = self.env['mrp.bom']._bom_find(product=product, bom_type='phantom')
if bom_kit:
+ kits |= product
boms, bom_sub_lines = bom_kit.explode(product, 1)
ratios_virtual_available = []
ratios_qty_available = []
@@ -115,8 +117,7 @@ class ProductProduct(models.Model):
product.qty_available = min(ratios_qty_available) // 1
product.incoming_qty = min(ratios_incoming_qty) // 1
product.outgoing_qty = min(ratios_incoming_qty) // 1
- else:
- super(ProductProduct, self)._compute_quantities()
+ super(ProductProduct, self - kits)._compute_quantities()
@api.multi
def action_view_bom(self):
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 62d35059978..f11a4f4a5c1 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -13,6 +13,7 @@ from odoo.addons.base.models import ir_module
from odoo.exceptions import ValidationError
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.misc import formatLang
+from odoo.http import request
_logger = logging.getLogger(__name__)
@@ -212,6 +213,19 @@ class PaymentAcquirer(models.Model):
(_check_required_if_provider, 'Required fields not filled', []),
]
+ def get_base_url(self):
+ self.ensure_one()
+ # priority is always given to url_root
+ # from the request
+ url = ''
+ if request:
+ url = request.httprequest.url_root
+
+ if not url and 'website_id' in self and self.website_id:
+ url = self.website_id._get_http_domain()
+
+ return url or self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
def _get_feature_support(self):
"""Get advanced feature support by provider.
diff --git a/addons/payment_stripe_sca/models/payment.py b/addons/payment_stripe_sca/models/payment.py
index 01093ab6d7f..fec16081c5d 100644
--- a/addons/payment_stripe_sca/models/payment.py
+++ b/addons/payment_stripe_sca/models/payment.py
@@ -20,7 +20,7 @@ class PaymentAcquirerStripeSCA(models.Model):
def stripe_form_generate_values(self, tx_values):
self.ensure_one()
- base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
+ base_url = self.get_base_url()
stripe_session_data = {
"payment_method_types[]": "card",
"line_items[][amount]": int(
@@ -177,6 +177,9 @@ class PaymentTransactionStripeSCA(models.Model):
"_stripe_create_payment_intent: Values received:\n%s", pprint.pformat(res)
)
return res
+ if not self.payment_token_id.stripe_payment_method:
+ # old token before installing stripe_sca, need to fetch data from the api
+ self.payment_token_id._stripe_sca_migrate_customer()
charge_params = {
"amount": int(
self.amount
@@ -366,3 +369,39 @@ class PaymentTokenStripeSCA(models.Model):
raise NotImplementedError(
"This method can no longer be used with the payment_stripe_sca module."
)
+
+ def _stripe_sca_migrate_customer(self):
+ """Migrate a token from the old implementation of Stripe to the SCA one.
+
+ In the old implementation, it was possible to create a valid charge just by
+ giving the customer ref to ask Stripe to use the default source (= default
+ card). Since we have a one-to-one matching between a saved card, this used to
+ work well - but now we need to specify the payment method for each call and so
+ we have to contact stripe to get the default source for the customer and save it
+ in the payment token.
+ This conversion will happen once per token, the first time it gets used following
+ the installation of the module."""
+ self.ensure_one()
+ url = "customers/%s" % (self.acquirer_ref)
+ data = self.acquirer_id._stripe_request(url, method="GET")
+ sources = data.get('sources', {}).get('data', [])
+ pm_ref = False
+ if sources:
+ if len(sources) > 1:
+ _logger.warning('stripe sca customer conversion: there should be a single saved source per customer!')
+ pm_ref = sources[0].get('id')
+ else:
+ url = 'payment_methods'
+ params = {
+ 'type': 'card',
+ 'customer': self.acquirer_ref,
+ }
+ payment_methods = self.acquirer_id._stripe_request(url, params, method='GET')
+ cards = payment_methods.get('data', [])
+ if len(cards) > 1:
+ _logger.warning('stripe sca customer conversion: there should be a single saved source per customer!')
+ pm_ref = cards and cards[0].get('id')
+ if not pm_ref:
+ raise ValidationError(_('Unable to convert Stripe customer for SCA compatibility. Is there at least one card for this customer in the Stripe backend?'))
+ self.stripe_payment_method = pm_ref
+ _logger.info('converted old customer ref to sca-compatible record for payment token %s', self.id) \ No newline at end of file
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ba2e4db2cd3..016356fcb32 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -754,7 +754,7 @@ class PosOrder(models.Model):
for order in self:
if order.lines and not order.amount_total:
continue
- if (not order.lines) or (not order.statement_ids) or (abs(order.amount_total - order.amount_paid) > 0.00001):
+ if (not order.lines) or (not order.statement_ids) or (not float_is_zero((order.amount_total - order.amount_paid), precision_rounding=order.pricelist_id.currency_id.rounding)):
return False
return True
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index 0797b05d4a5..4e432dd6ce1 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -130,7 +130,7 @@ class Pricelist(models.Model):
""" Low-level method - Mono pricelist, multi products
Returns: dict{product_id: (price, suitable_rule) for the given pricelist}
- If date in context: Date of the pricelist (%Y-%m-%d)
+ Date in context can be a date, datetime, ...
:param products_qty_partner: list of typles products, quantity, partner
:param datetime date: validity date
@@ -138,8 +138,8 @@ class Pricelist(models.Model):
"""
self.ensure_one()
if not date:
- date = self._context.get('date') or fields.Date.context_today(self)
- date = fields.Date.to_date(date) # handwritten query below does not work with non-dates (e.g. datetimes)
+ date = self._context.get('date') or fields.Date.today()
+ date = fields.Date.to_date(date) # boundary conditions differ if we have a datetime
if not uom_id and self._context.get('uom'):
uom_id = self._context['uom']
if uom_id:
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 5867cce9092..d1bcc1adb3a 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -400,7 +400,7 @@ class ProductTemplate(models.Model):
@api.multi
def name_get(self):
# Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
- self.read(['name', 'default_code'])
+ self.browse(self.ids).read(['name', 'default_code'])
return [(template.id, '%s%s' % (template.default_code and '[%s] ' % template.default_code or '', template.name))
for template in self]
diff --git a/addons/snailmail/models/res_partner.py b/addons/snailmail/models/res_partner.py
index c4b3b576bf0..493846c865d 100644
--- a/addons/snailmail/models/res_partner.py
+++ b/addons/snailmail/models/res_partner.py
@@ -11,15 +11,16 @@ class ResPartner(models.Model):
@api.multi
def write(self, vals):
- letter_address_vals = {}
- address_fields = ['street', 'street2', 'city', 'zip', 'state_id', 'country_id']
- for field in address_fields:
- if field in vals:
- letter_address_vals[field] = vals[field]
+ for id in self.ids:
+ letter_address_vals = {}
+ address_fields = ['street', 'street2', 'city', 'zip', 'state_id', 'country_id']
+ for field in address_fields:
+ if field in vals:
+ letter_address_vals[field] = vals[field]
- if len(letter_address_vals):
- letter_ids = self.env['snailmail.letter'].search([('state', 'not in', ['sent', 'canceled']), ('partner_id', '=', self.id)])
- letter_ids.write(letter_address_vals)
+ if len(letter_address_vals):
+ letter_ids = self.env['snailmail.letter'].search([('state', 'not in', ['sent', 'canceled']), ('partner_id', '=', id)])
+ letter_ids.write(letter_address_vals)
return super(ResPartner, self).write(vals)
@@ -40,4 +41,4 @@ class ResPartner(models.Model):
if self.env.context.get('snailmail_layout') and self.street2:
return "%(street)s, %(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"
- return super(ResPartner, self)._get_address_format() \ No newline at end of file
+ return super(ResPartner, self)._get_address_format()
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 6b219fa71d7..879274ed3e1 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -896,6 +896,7 @@ class StockMove(models.Model):
# cache invalidation when actually reserving the move.
reserved_availability = {move: move.reserved_availability for move in self}
roundings = {move: move.product_id.uom_id.rounding for move in self}
+ move_line_vals_list = []
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
rounding = roundings[move]
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
@@ -905,7 +906,7 @@ class StockMove(models.Model):
# create the move line(s) but do not impact quants
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
for i in range(0, int(missing_reserved_quantity)):
- self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
+ move_line_vals_list.append(move._prepare_move_line_vals(quantity=1))
else:
to_update = move.move_line_ids.filtered(lambda ml: ml.product_uom_id == move.product_uom and
ml.location_id == move.location_id and
@@ -917,7 +918,7 @@ class StockMove(models.Model):
if to_update:
to_update[0].product_uom_qty += missing_reserved_uom_quantity
else:
- self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
+ move_line_vals_list.append(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
assigned_moves |= move
else:
if not move.move_orig_ids:
@@ -1008,6 +1009,7 @@ class StockMove(models.Model):
assigned_moves |= move
break
partially_available_moves |= move
+ self.env['stock.move.line'].create(move_line_vals_list)
partially_available_moves.write({'state': 'partially_available'})
assigned_moves.write({'state': 'assigned'})
self.mapped('picking_id')._check_entire_pack()
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 653f77eef16..64ee09445bf 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -644,21 +644,31 @@ class Picking(models.Model):
@api.depends('state', 'move_lines', 'move_lines.state', 'move_lines.package_level_id', 'move_lines.move_line_ids.package_level_id')
def _compute_move_without_package(self):
for picking in self:
- move_ids_without_package = self.env['stock.move']
- if picking.picking_type_entire_packs == False:
- move_ids_without_package = picking.move_lines
- else:
- for move in picking.move_lines:
- if not move.package_level_id:
- if move.state in ('assigned', 'done'):
- if any(not ml.package_level_id for ml in move.move_line_ids):
- move_ids_without_package |= move
- else:
- move_ids_without_package |= move
- picking.move_ids_without_package = move_ids_without_package.filtered(lambda move: not move.scrap_ids)
+ picking.move_ids_without_package = picking._get_move_ids_without_package()
def _set_move_without_package(self):
- self.move_lines |= self.move_ids_without_package
+ new_mwp = self[0].move_ids_without_package
+ for picking in self:
+ old_mwp = picking._get_move_ids_without_package()
+ picking.move_lines = (picking.move_lines - old_mwp) | new_mwp
+ moves_to_unlink = old_mwp - new_mwp
+ if moves_to_unlink:
+ moves_to_unlink.unlink()
+
+ def _get_move_ids_without_package(self):
+ self.ensure_one()
+ move_ids_without_package = self.env['stock.move']
+ if not self.picking_type_entire_packs:
+ move_ids_without_package = self.move_lines
+ else:
+ for move in self.move_lines:
+ if not move.package_level_id:
+ if move.state in ('assigned', 'done'):
+ if any(not ml.package_level_id for ml in move.move_line_ids):
+ move_ids_without_package |= move
+ else:
+ move_ids_without_package |= move
+ return move_ids_without_package.filtered(lambda move: not move.scrap_ids)
def _check_move_lines_map_quant_package(self, package):
""" This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index c0cbf4db0e0..34ebbb905f6 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -1831,6 +1831,20 @@ class TestSinglePicking(TestStockCommon):
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
self.assertFalse(back_order, 'There should be no back order')
+ def test_unlink_move_1(self):
+ picking = Form(self.env['stock.picking'])
+ ptout = self.env['stock.picking.type'].browse(self.picking_type_out)
+ picking.picking_type_id = ptout
+ with picking.move_ids_without_package.new() as move:
+ move.product_id = self.productA
+ move.product_uom_qty = 10
+ picking = picking.save()
+
+ picking = Form(picking)
+ picking.move_ids_without_package.remove(0)
+ picking = picking.save()
+ self.assertEqual(len(picking.move_ids_without_package), 0)
+
class TestStockUOM(TestStockCommon):
def setUp(self):
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index e8552c45b7a..73aaedb45d1 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -403,10 +403,11 @@ class Survey(http.Controller):
answer_tag = "%s_%s" % (survey_sudo.id, question.id)
request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
+ vals = {}
if answer_sudo.is_time_limit_reached or survey_sudo.questions_layout == 'one_page':
go_back = False
answer_sudo._mark_done()
- else:
+ elif 'button_submit' in post:
go_back = post['button_submit'] == 'previous'
next_page, last = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id, go_back=go_back)
vals = {'last_displayed_page_id': page_or_question_id}
@@ -415,11 +416,16 @@ class Survey(http.Controller):
answer_sudo._mark_done()
else:
vals.update({'state': 'skip'})
- answer_sudo.write(vals)
- ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
- if go_back:
- ret['redirect'] += '?prev=prev'
+ if 'breadcrumb_redirect' in post:
+ ret['redirect'] = post['breadcrumb_redirect']
+ else:
+ if vals:
+ answer_sudo.write(vals)
+
+ ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
+ if go_back:
+ ret['redirect'] += '?prev=prev'
return json.dumps(ret)
diff --git a/addons/survey/static/src/js/survey.js b/addons/survey/static/src/js/survey.js
index d232c3073f7..86d8728d9be 100644
--- a/addons/survey/static/src/js/survey.js
+++ b/addons/survey/static/src/js/survey.js
@@ -129,6 +129,15 @@ if(!the_form.length) {
}
}
+ $('.o_survey_header .breadcrumb-item a').each(function() {
+ var $valObj = $(this);
+ $valObj.click(function(event) {
+ event.preventDefault();
+ $('<input>').attr({type: 'hidden', name: 'breadcrumb_redirect', value: $valObj.attr('href')}).appendTo('.js_surveyform');
+ $('.js_surveyform').submit();
+ });
+ });
+
// Parameters for form submission
$('.js_surveyform').ajaxForm({
url: submit_controller,
@@ -229,5 +238,4 @@ if(!the_form.length) {
display_scores();
}
});
-
});
diff --git a/addons/web/static/src/css/reset.min.css b/addons/web/static/src/css/reset.min.css
index 97c0b78b002..b6163764a5c 100644
--- a/addons/web/static/src/css/reset.min.css
+++ b/addons/web/static/src/css/reset.min.css
@@ -55,10 +55,6 @@ ul ul {
list-style: circle;
}
-blockquote, q {
- quotes: none;
-}
-
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 7be17700f98..e369cea9c8c 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -540,7 +540,7 @@ var dom = {
menuItemsWidth += computeFloatOuterWidthWithMargins($extraItemsToggle[0]);
do {
menuItemsWidth -= computeFloatOuterWidthWithMargins($items.eq(--nbItems)[0]);
- } while (!(maxWidth - menuItemsWidth >= -0.001));
+ } while (!(maxWidth - menuItemsWidth >= -0.001) && (nbItems > 0));
var $extraItems = $items.slice(nbItems).detach();
$extraItems.removeClass('nav-item');
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index dc456d21ae0..317ea30e0ad 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -533,6 +533,12 @@ var AbstractField = Widget.extend({
}
break;
case $.ui.keyCode.ENTER:
+ // We preventDefault the ENTER key because of two coexisting behaviours:
+ // - In HTML5, pressing ENTER on a <button> triggers two events: a 'keydown' AND a 'click'
+ // - When creating and opening a dialog, the focus is automatically given to the primary button
+ // The end result caused some issues where a modal opened by an ENTER keypress (e.g. saving
+ // changes in multiple edition) confirmed the modal without any intentionnal user input.
+ ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'next_line'});
break;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index eeda3d220b6..93a636c141a 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -492,6 +492,9 @@ var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
supportedFieldTypes: ['date', 'datetime'],
+ // we don't need to listen on 'input' nor 'change' events because the
+ // datepicker widget is already listening, and will correctly notify changes
+ events: AbstractField.prototype.events,
/**
* @override
@@ -888,6 +891,8 @@ var FieldBoolean = AbstractField.extend({
_onKeydown: function (ev) {
switch (ev.which) {
case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
this.$input.prop('checked', !this.value);
this._setValue(!this.value);
return;
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index ec99f4fa0f6..fd73b7a9ed2 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -415,7 +415,7 @@ function parseDate(value, field, options) {
if (options && options.isUTC) {
date = moment.utc(value);
} else {
- date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
+ date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601]);
}
if (date.isValid()) {
if (date.year() === 0) {
@@ -459,7 +459,7 @@ function parseDateTime(value, field, options) {
// phatomjs crash if we don't use this format
datetime = moment.utc(value.replace(' ', 'T') + 'Z');
} else {
- datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
+ datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601]);
if (options && options.timezone) {
datetime.add(-session.getTZOffset(datetime), 'minutes');
}
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index be35395aab7..6213fc8de6a 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -395,8 +395,10 @@ var FieldMany2One = AbstractField.extend({
no_create: !self.can_create,
on_selected: function (records) {
self.reinitialize(records[0]);
+ },
+ on_closed: function () {
self.activate();
- }
+ },
};
},
/**
@@ -805,6 +807,48 @@ var Many2oneBarcode = FieldMany2One.extend({
});
var ListFieldMany2One = FieldMany2One.extend({
+ events: _.extend({}, FieldMany2One.prototype.events, {
+ 'focusin input': '_onInputFocusin',
+ }),
+
+ /**
+ * Should never be allowed to be opened while in readonly mode in a list
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ // when we empty the input, we delay the setValue to prevent from
+ // triggering the 'fieldChanged' event twice when the user wants set
+ // another m2o value ; the following attribute is used to determine when
+ // we skipped the setValue, s.t. we can perform it later on if the user
+ // didn't select another value
+ this.mustSetValue = false;
+ this.m2oDialogFocused = false;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * If in readonly, will never be considered as an active widget.
+ *
+ * @override
+ */
+ activate: function () {
+ if (this.mode === 'readonly') {
+ return false;
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ reinitialize: function () {
+ this.mustSetValue = false;
+ return this._super.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Private
@@ -816,6 +860,52 @@ var ListFieldMany2One = FieldMany2One.extend({
_renderReadonly: function () {
this.$el.text(this.m2o_value);
},
+ /**
+ * @override
+ * @private
+ */
+ _searchCreatePopup: function () {
+ this.m2oDialogFocused = true;
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onInputFocusin: function () {
+ this.m2oDialogFocused = false;
+ },
+ /**
+ * In list views, we don't want to try to trigger a fieldChange when the field
+ * is being emptied. Instead, it will be triggered as the user leaves the field
+ * while it is empty.
+ *
+ * @override
+ * @private
+ */
+ _onInputFocusout: function () {
+ this._super.apply(this, arguments);
+ if (!this.m2oDialogFocused && this.$input.val() === "" && this.mustSetValue) {
+ this.reinitialize(false);
+ }
+ },
+ /**
+ * Prevents the triggering of an immediate _onFieldChanged when emptying the field.
+ *
+ * @override
+ * @private
+ */
+ _onInputKeyup: function () {
+ if (this.$input.val() !== "") {
+ this._super.apply(this, arguments);
+ } else {
+ this.mustSetValue = true;
+ }
+ },
});
var KanbanFieldMany2One = AbstractField.extend({
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index b864063ee92..e0598fcf9cc 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -50,6 +50,21 @@ var ListController = BasicController.extend({
this.editable = params.editable;
this.noLeaf = params.noLeaf;
this.selectedRecords = params.selectedRecords || [];
+ this.multipleRecordsSavingPromise = null;
+ this.fieldChangedPrevented = false;
+ this._onMouseupWindowDiscard = null;
+ },
+
+ /**
+ * Overriden to properly unbind any mouseup listeners still bound to the window
+ *
+ * @override
+ */
+ destroy: function () {
+ if (this._onMouseupWindowDiscard) {
+ window.removeEventListener('mouseup', this._onMouseupWindowDiscard, true);
+ }
+ return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
@@ -134,6 +149,7 @@ var ListController = BasicController.extend({
},
trigger: 'manual',
});
+ this.$buttons.on('mousedown', '.o_list_button_discard', this._onDiscardMousedown.bind(this));
this.$buttons.on('click', '.o_list_button_discard', this._onDiscard.bind(this));
this.$buttons.appendTo($node);
}
@@ -342,15 +358,6 @@ var ListController = BasicController.extend({
return _.extend(env, {domain: record.getDomain()});
},
/**
- * @private
- * @returns {boolean}
- */
- _inMultipleRecordEdition: function (recordId) {
- var record = this.model.get(recordId, { raw: true });
- var recordIds = _.union([recordId], this.selectedRecords);
- return recordIds.length > 1 && record.res_id;
- },
- /**
* Only display the pager when there are data to display.
*
* @override
@@ -361,13 +368,20 @@ var ListController = BasicController.extend({
return !!state.count;
},
/**
+ * Saves multiple records at once. This method is called by the _onFieldChanged method
+ * since the record must be confirmed as soon as the focus leaves a dirty cell.
+ * Pseudo-validation is performed with registered modifiers.
+ * Returns a promise that is resolved when confirming and rejected in any other case.
+ *
* @private
* @param {string} recordId
* @param {Object} node
* @param {Object} changes
+ * @returns {Promise}
*/
_saveMultipleRecords: function (recordId, node, changes) {
var self = this;
+ var fieldName = Object.keys(changes)[0];
var value = Object.values(changes)[0];
var recordIds = _.union([recordId], this.selectedRecords);
var validRecordIds = recordIds.reduce(function (result, recordId) {
@@ -378,22 +392,52 @@ var ListController = BasicController.extend({
}
return result;
}, []);
- var message = _.str.sprintf(
- _t('Do you want to set the value on the %d valid selected records?'),
- validRecordIds.length);
- if (recordIds.length !== validRecordIds.length) {
- var nbInvalid = recordIds.length - validRecordIds.length;
- message += ' ' + _.str.sprintf(_t('(%d invalid)'), nbInvalid);
- }
- Dialog.confirm(this, message, {
- confirm_callback: function () {
- self.model.saveRecords(recordId, validRecordIds)
- .then(function () {
- self._updateButtons('readonly');
- var state = self.model.get(self.handle);
- self.renderer.updateState(state, {});
- });
- },
+ var nbInvalid = recordIds.length - validRecordIds.length;
+
+ return new Promise(function (resolve, reject) {
+ var dialog;
+ var rejectAndDiscard = function () {
+ self.model.discardChanges(recordId);
+ return self._confirmSave(recordId).then(reject);
+ };
+ if (validRecordIds.length > 0) {
+ var message;
+ if (nbInvalid === 0) {
+ message = _.str.sprintf(
+ _t("Do you want to set the value on the %d selected records?"),
+ validRecordIds.length);
+ } else {
+ message = _.str.sprintf(
+ _t("Do you want to set the value on the %d valid selected records? (%d invalid)"),
+ validRecordIds.length, nbInvalid);
+ }
+ dialog = Dialog.confirm(self, message, {
+ confirm_callback: function () {
+ return self.model.saveRecords(recordId, validRecordIds, fieldName)
+ .then(function () {
+ self._updateButtons('readonly');
+ var state = self.model.get(self.handle);
+ return self.renderer.updateState(state, {}).then(function () {
+ resolve(Object.keys(changes));
+ });
+ })
+ .guardedCatch(rejectAndDiscard);
+ },
+ cancel_callback: rejectAndDiscard,
+ });
+ } else {
+ dialog = Dialog.alert(self, _t("No valid record to save"), {
+ confirm_callback: rejectAndDiscard,
+ });
+ }
+ dialog.on('closed', self, function () {
+ // we need to wait for the dialog to be actually closed, but
+ // the 'closed' event is triggered just before, and it prevents
+ // from focussing the cell
+ Promise.resolve().then(function () {
+ self.renderer.focusCell(recordId, node);
+ });
+ });
});
},
/**
@@ -404,10 +448,11 @@ var ListController = BasicController.extend({
*/
_saveRecord: function (recordId) {
var record = this.model.get(recordId, { raw: true });
- if (record.isDirty() && this._inMultipleRecordEdition(recordId)) {
+ if (record.isDirty() && this.renderer.inMultipleRecordEdition(recordId)) {
// do not save the record (see _saveMultipleRecords)
- return Promise.resolve();
-
+ var prom = this.multipleRecordsSavingPromise || Promise.reject();
+ this.multipleRecordsSavingPromise = null;
+ return prom;
}
return this._super.apply(this, arguments);
},
@@ -563,6 +608,31 @@ var ListController = BasicController.extend({
this._discardChanges();
},
/**
+ * Used to detect if the discard button is about to be clicked.
+ * Some focusout events might occur and trigger a save which
+ * is not always wanted when clicking "Discard".
+ *
+ * @param {MouseEvent} ev
+ * @private
+ */
+ _onDiscardMousedown: function (ev) {
+ var self = this;
+ this.fieldChangedPrevented = true;
+ this._onMouseupWindowDiscard = (function (mouseupEvent) {
+ window.removeEventListener('mouseup', self._onMouseupWindowDiscard, true);
+ self._onMouseupWindowDiscard = null;
+ var preventedEvent = self.fieldChangedPrevented;
+ self.fieldChangedPrevented = false;
+ // If the user starts clicking (mousedown) on the button and stops clicking
+ // (mouseup) outside of the button, we want to trigger the original onFieldChanged
+ // Event that was prevented in the meantime.
+ if (ev.target !== mouseupEvent.target && preventedEvent.constructor.name === 'OdooEvent') {
+ self._onFieldChanged(preventedEvent);
+ }
+ }).bind(this);
+ window.addEventListener('mouseup', this._onMouseupWindowDiscard, true);
+ },
+ /**
* Called when the user asks to edit a row -> Updates the controller buttons
*
* @param {OdooEvent} ev
@@ -618,15 +688,18 @@ var ListController = BasicController.extend({
_onFieldChanged: function (ev) {
ev.stopPropagation();
var self = this;
+ var recordId = ev.data.dataPointID;
- if (this._inMultipleRecordEdition(ev.data.dataPointID)) {
+ if (this.fieldChangedPrevented) {
+ this.fieldChangedPrevented = ev;
+ } else if (this.renderer.inMultipleRecordEdition(recordId)) {
// deal with edition of multiple lines
- var _onSuccess = ev.data.onSuccess;
ev.data.onSuccess = function () {
- Promise.resolve(_onSuccess()).then(function () {
+ self.multipleRecordsSavingPromise =
self._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes);
- });
};
+ // disable onchanges as we'll save directly
+ ev.data.notifyChange = false;
}
this._super.apply(this, arguments);
},
@@ -653,6 +726,29 @@ var ListController = BasicController.extend({
this._toggleSidebar();
},
/**
+ * If the record is set as dirty while in multiple record edition,
+ * we want to immediatly discard the change.
+ *
+ * @private
+ * @override
+ * @param {OdooEvent} ev
+ */
+ _onSetDirty: function (ev) {
+ var self = this;
+ var recordId = ev.data.dataPointID;
+ if (this.renderer.inMultipleRecordEdition(recordId)) {
+ ev.stopPropagation();
+ Dialog.alert(this, _t("No valid record to save"), {
+ confirm_callback: function () {
+ self.model.discardChanges(recordId);
+ self._confirmSave(recordId);
+ },
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
* When the user clicks on one of the sortable column headers, we need to
* tell the model to sort itself properly, to update the pager and to
* rerender the view.
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index f87268ddb1e..4d5fd24124e 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -277,6 +277,17 @@ ListRenderer.include({
this._selectCell(rowIndex, 0);
},
/**
+ * Gives focus to a specific cell, given its row and its related column.
+ *
+ * @param {string} recordId
+ * @param {Object} column
+ */
+ focusCell: function (recordId, column) {
+ var $row = this._getRow(recordId);
+ var cellIndex = this.columns.indexOf(column);
+ $row.find('.o_data_cell')[cellIndex].focus();
+ },
+ /**
* Returns the recordID associated to the line which is currently in edition
* or null if there is no line in edition.
*
@@ -289,6 +300,16 @@ ListRenderer.include({
return null;
},
/**
+ * Returns whether the list is in multiple record edition from a given record.
+ *
+ * @private
+ * @param {string} recordId
+ * @returns {boolean}
+ */
+ inMultipleRecordEdition: function (recordId) {
+ return this.editable && this.selection.length > 1 && this.selection.indexOf(recordId) > -1;
+ },
+ /**
* Removes the line associated to the given recordID (the index of the row
* is found thanks to the old state), then updates the state.
*
@@ -303,6 +324,7 @@ ListRenderer.include({
}
if ($row.prop('rowIndex') - 1 === this.currentRow) {
this.currentRow = null;
+ this._enableRecordSelectors();
}
// destroy widgets first
@@ -397,7 +419,11 @@ ListRenderer.include({
// Toggle selected class here so that style is applied at the end
$row.toggleClass('o_selected_row', editMode);
- $row.find('.o_list_record_selector input').prop('disabled', !record.res_id);
+ if (editMode) {
+ this._disableRecordSelectors();
+ } else {
+ this._enableRecordSelectors();
+ }
return Promise.all(defs).then(function () {
// necessary to trigger resize on fieldtexts
@@ -427,17 +453,21 @@ ListRenderer.include({
var recordWidgets = this.allFieldWidgets[recordID];
toggleWidgets(true);
- var prom = new Promise(function (resolve, reject) {
+ return new Promise(function (resolve, reject) {
self.trigger_up('save_line', {
recordID: recordID,
onSuccess: resolve,
onFailure: reject,
});
- });
- prom.guardedCatch(function() {
+ }).then(function (changedFields) {
+ self._enableRecordSelectors();
+ // If any field has changed and if the list is in multiple edition,
+ // we send a truthy boolean to _selectRow to tell it not to select
+ // the following record.
+ return changedFields && changedFields.length && self.inMultipleRecordEdition(recordID);
+ }).guardedCatch(function () {
toggleWidgets(false);
});
- return prom;
function toggleWidgets(disabled) {
_.each(recordWidgets, function (widget) {
@@ -464,6 +494,14 @@ ListRenderer.include({
//--------------------------------------------------------------------------
/**
+ * When editing a row, we want to disable all record selectors.
+ *
+ * @private
+ */
+ _disableRecordSelectors: function () {
+ this.$('.o_list_record_selector input').attr('disabled', 'disabled');
+ },
+ /**
* Destroy all field widgets corresponding to a record. Useful when we are
* removing a useless row.
*
@@ -477,6 +515,12 @@ ListRenderer.include({
}
},
/**
+ * @private
+ */
+ _enableRecordSelectors: function () {
+ this.$('.o_list_record_selector input').attr('disabled', false);
+ },
+ /**
* Returns the relative width according to the widget or the field type.
* @see _renderHeader
*
@@ -992,7 +1036,10 @@ ListRenderer.include({
var recordId = this._getRecordID(rowIndex);
// To select a row, the currently selected one must be unselected first
var self = this;
- return this.unselectRow().then(function () {
+ return this.unselectRow().then(function (noSelectNext) {
+ if (noSelectNext) {
+ return Promise.resolve();
+ }
if (!recordId) {
// The row to selected doesn't exist anymore (probably because
// an onchange triggered when unselecting the previous one
@@ -1003,7 +1050,10 @@ ListRenderer.include({
return new Promise(function (resolve) {
self.trigger_up('edit_line', {
recordId: recordId,
- onSuccess: resolve,
+ onSuccess: function () {
+ self._disableRecordSelectors();
+ resolve();
+ },
});
});
});
@@ -1035,7 +1085,10 @@ ListRenderer.include({
ev.stopPropagation();
var self = this;
- var groupId = $(ev.target).data('group-id');
+ // This method can be called when selecting the parent of the link.
+ // We need to ensure that the link is the actual target
+ var target = ev.target.tagName !== 'A' ? ev.target.getElementsByTagName('A')[0] : ev.target;
+ var groupId = target.dataset.groupId;
this.currentGroupId = groupId;
this.unselectRow().then(function () {
self.trigger_up('add_record', {
@@ -1213,12 +1266,13 @@ ListRenderer.include({
this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID,
onSuccess: function () {
+ self._enableRecordSelectors();
var recordId = self._getRecordID(rowIndex);
if (recordId) {
var correspondingRow = self._getRow(recordId);
correspondingRow.children().eq(cellIndex).focus();
} else if (self.currentGroupId) {
- self.$('a[data-group-id=' + self.currentGroupId + ']').focus();
+ self.$('a[data-group-id="' + self.currentGroupId + '"]').focus();
} else {
self.$('.o_field_x2many_list_row_add a:first').focus(); // FIXME
}
diff --git a/addons/web/static/src/js/views/list/list_model.js b/addons/web/static/src/js/views/list/list_model.js
index 25fdf28954c..7a2e0c51ada 100644
--- a/addons/web/static/src/js/views/list/list_model.js
+++ b/addons/web/static/src/js/views/list/list_model.js
@@ -40,12 +40,18 @@ odoo.define('web.ListModel', function (require) {
* @param {string} referenceRecordId the record datapoint used to
* generate the changes to apply to recordIds
* @param {string[]} recordIds a list of record datapoint ids
+ * @param {string} fieldName the field to write
*/
- saveRecords: function (referenceRecordId, recordIds) {
+ saveRecords: function (referenceRecordId, recordIds, fieldName) {
var self = this;
var referenceRecord = this.localData[referenceRecordId];
var list = this.localData[referenceRecord.parentID];
- var changes = this._generateChanges(referenceRecord, {});
+ // generate all record values to ensure that we'll write something
+ // (e.g. 2 records selected, edit a many2one in the first one, but
+ // reset same value, we still want to save this value on the other
+ // record)
+ var allChanges = this._generateChanges(referenceRecord, {changesOnly: false});
+ var changes = _.pick(allChanges, fieldName);
var records = recordIds.map(function (recordId) {
return self.localData[recordId];
});
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index afa8f09a857..8aaf09fc4c7 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -417,6 +417,7 @@ var ListRenderer = BasicRenderer.extend({
data: record.data,
escape: true,
isPassword: 'password' in node.attrs,
+ digits: node.attrs.digits ? JSON.parse(node.attrs.digits) : undefined,
});
this._handleAttributes($td, node);
var title = field.type !== 'boolean' ? formattedValue : '';
@@ -979,11 +980,18 @@ var ListRenderer = BasicRenderer.extend({
* @private
*/
_updateSelection: function () {
- var $selectedRows = this.$('tbody .o_list_record_selector input:checked')
- .closest('tr');
- this.selection = _.map($selectedRows, function (row) {
- return $(row).data('id');
+ this.selection = [];
+ var self = this;
+ var $inputs = this.$('tbody .o_list_record_selector input:visible:not(:disabled)');
+ var allChecked = $inputs.length > 0;
+ $inputs.each(function (index, input) {
+ if (input.checked) {
+ self.selection.push($(input).closest('tr').data('id'));
+ } else {
+ allChecked = false;
+ }
});
+ this.$('thead .o_list_record_selector input').prop('checked', allChecked);
this.trigger_up('selection_changed', { selection: this.selection });
this._updateFooter();
},
@@ -1156,9 +1164,6 @@ var ListRenderer = BasicRenderer.extend({
_onSelectRecord: function (ev) {
ev.stopPropagation();
this._updateSelection();
- if (!$(ev.currentTarget).find('input').prop('checked')) {
- this.$('thead .o_list_record_selector input').prop('checked', false);
- }
},
/**
* @private
@@ -1175,11 +1180,13 @@ var ListRenderer = BasicRenderer.extend({
_onToggleGroup: function (ev) {
ev.preventDefault();
ev.stopPropagation();
+ var self = this;
var group = $(ev.currentTarget).closest('tr').data('group');
if (group.count) {
this.trigger_up('toggle_group', {
group: group,
onSuccess: function() {
+ self._updateSelection();
// Refocus the header after re-render unless the user
// already focused something else by now
if (document.activeElement.tagName === 'BODY') {
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index f665bf49791..abfad01558e 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -329,6 +329,7 @@ var SelectCreateDialog = ViewDialog.extend({
this._super.apply(this, arguments);
_.defaults(this.options, { initial_view: 'search' });
this.on_selected = this.options.on_selected || (function () {});
+ this.on_closed = this.options.on_closed || (function () {});
this.initialIDs = this.options.initial_ids;
},
@@ -412,6 +413,10 @@ var SelectCreateDialog = ViewDialog.extend({
return fragment;
});
},
+ close: function () {
+ this._super.apply(this, arguments);
+ this.on_closed();
+ },
create_edit_record: function () {
var self = this;
var dialog = new FormViewDialog(this, _.extend({}, this.options, {
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index ac8e83c8e30..bde1d793374 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -12,7 +12,6 @@ var DateWidget = Widget.extend({
template: "web.datepicker",
type_of_date: "date",
events: {
- 'change.datetimepicker': 'changeDatetime',
'error.datetimepicker': 'errorDatetime',
'change .o_datepicker_input': 'changeDatetime',
'click input': '_onInputClicked',
@@ -92,6 +91,13 @@ var DateWidget = Widget.extend({
* set datetime value
*/
changeDatetime: function () {
+ if (this.__libInput > 0) {
+ if (this.options.warn_future) {
+ this._warnFuture(this.getValue());
+ }
+ this.trigger("datetime_changed");
+ return;
+ }
var oldValue = this.getValue();
if (this.isValid()) {
this._setValueFromUi();
@@ -254,6 +260,7 @@ var DateWidget = Widget.extend({
*/
_onDateTimePickerHide: function () {
this.__isOpen = false;
+ this.changeDatetime();
if (this._onScroll) {
window.removeEventListener('scroll', this._onScroll, true);
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 5eff2fb4422..657ad2d6ebb 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -712,8 +712,8 @@
<t t-name="FieldMany2One">
<t t-if="widget.mode === 'readonly'">
- <a t-if="!widget.nodeOptions.no_open" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/>
- <span t-if="widget.nodeOptions.no_open"/>
+ <a t-if="!widget.noOpen" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/>
+ <span t-if="widget.noOpen"/>
</t>
<div t-if="widget.mode === 'edit'" class="o_field_widget o_field_many2one" aria-atomic="true">
<div class="o_input_dropdown">
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index c4869ce2e00..8e6c05b938f 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -529,6 +529,42 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('float field in list view no widget', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Float field should be considered set for value 0.');
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=qux]').val(), '0.000',
+ 'The value should be rendered with correct precision.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598');
+ assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
QUnit.test('float field in form view', async function (assert) {
assert.expect(5);
@@ -3093,6 +3129,63 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datetime fields do not trigger fieldChange before datetime completly picked', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges = {
+ datetime: function () {},
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="datetime"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step('onchange');
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
+
+ // select a date and time
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(3));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day:contains(22)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .fa-clock-o'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-hour'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .hour:contains(08)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-minute'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .minute:contains(25)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-second'));
+ assert.verifySteps([], "should not have done any onchange yet");
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)'));
+
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), "04/22/2017 08:25:35");
+ assert.verifySteps(['onchange'], "should have done only one onchange");
+
+ form.destroy();
+ });
QUnit.test('datetime field not visible in form view should not capture the focus on keyboard navigation', async function (assert) {
assert.expect(1);
@@ -3215,6 +3308,45 @@ QUnit.module('basic_fields', {
list.destroy();
});
+ QUnit.test('multi edition of datetime field in list view: edit date in input', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="datetime"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ // select two records and edit them
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell'));
+ assert.containsOnce(list, 'input.o_datepicker_input');
+ list.$('.o_datepicker_input').val("10/02/2019 09:00:00");
+ await testUtils.dom.triggerEvents(list.$('.o_datepicker_input'), ['change']);
+
+ assert.containsOnce(document.body, '.modal');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first .o_data_cell').text(), "10/02/2019 09:00:00");
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), "10/02/2019 09:00:00");
+
+ list.destroy();
+ });
+
QUnit.test('datetime field remove value', async function (assert) {
assert.expect(4);
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index c4281b38c87..e0b9564d13c 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -353,5 +353,39 @@ QUnit.test('parse datetime', function (assert) {
core._t.database.parameters = originalParameters;
});
+QUnit.test('parse date without separator', function (assert) {
+ assert.expect(8);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {date_format: '%d.%m/%Y'});
+ var dateFormat = "DD.MM/YYYY";
+
+ assert.throws(function () {fieldUtils.parse.date("1197")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.throws(function () {fieldUtils.parse.date("0131")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.throws(function () {fieldUtils.parse.date("970131")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.equal(fieldUtils.parse.date("3101").format(dateFormat), "31.01/" + moment.utc().year());
+ assert.equal(fieldUtils.parse.date("31.01").format(dateFormat), "31.01/" + moment.utc().year());
+ assert.equal(fieldUtils.parse.date("310197").format(dateFormat), "31.01/1997");
+ assert.equal(fieldUtils.parse.date("310117").format(dateFormat), "31.01/2017");
+ assert.equal(fieldUtils.parse.date("31011985").format(dateFormat), "31.01/1985");
+
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test('parse datetime without separator', function (assert) {
+ assert.expect(3);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {date_format: '%d.%m/%Y', time_format: '%H:%M/%S'});
+ var dateTimeFormat = "DD.MM/YYYY HH:mm/ss";
+
+ assert.equal(fieldUtils.parse.datetime("3101198508").format(dateTimeFormat), "31.01/1985 08:00/00");
+ assert.equal(fieldUtils.parse.datetime("310119850833").format(dateTimeFormat), "31.01/1985 08:33/00");
+ assert.equal(fieldUtils.parse.datetime("31/01/1985 08").format(dateTimeFormat), "31.01/1985 08:00/00");
+
+ core._t.database.parameters = originalParameters;
+});
});
});
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
index b32f8478ccf..e905e486f6f 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
@@ -566,6 +566,81 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test("many2one doesn't trigger field_change when being emptied", async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="trululu"/>
+ </tree>`,
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const $input = list.$('.o_field_widget[name=trululu] input');
+
+ await testUtils.fields.editInput($input, "");
+ await testUtils.dom.triggerEvents($input, ['keyup']);
+
+ assert.containsNone(document.body, '.modal',
+ "No save should be triggered when removing value");
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+
+ assert.containsOnce(document.body, '.modal',
+ "Saving should be triggered when selecting a value");
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ list.destroy();
+ });
+
+ QUnit.test("focus tracking on a many2one in a list", async function (assert) {
+ assert.expect(4);
+
+ const list = await createView({
+ arch: '<tree editable="top"><field name="trululu"/></tree>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
+ },
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const input = list.$('.o_data_row:first() .o_data_cell:first() input')[0];
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused when activated");
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickItem('trululu', 'Create');
+
+ // At this point, if the focus is correctly registered by the m2o, there
+ // should be only one modal (the "Create" one) and none for saving changes.
+ assert.containsOnce(document.body, '.modal', "There should be only one modal");
+
+ await testUtils.dom.click($('.modal .btn:not(.btn-primary)'));
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused after dialog closes");
+ assert.strictEqual(input.value, "", "Input should be empty after discard");
+
+ list.destroy();
+ });
+
QUnit.test('many2one fields with option "no_open"', async function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 0cf3279aa54..df25691f375 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -190,7 +190,7 @@ QUnit.module('Views', {
});
QUnit.test('simple editable rendering', async function (assert) {
- assert.expect(12);
+ assert.expect(15);
var list = await createView({
View: ListView,
@@ -201,6 +201,7 @@ QUnit.module('Views', {
assert.containsN(list, 'th', 3, "should have 2 th");
assert.containsN(list, 'th', 3, "should have 3 th");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
assert.containsOnce(list, 'td:contains(yop)', "should contain yop");
assert.isVisible(list.$buttons.find('.o_list_button_add'),
@@ -218,6 +219,7 @@ QUnit.module('Views', {
"should have a visible save button");
assert.isVisible(list.$buttons.find('.o_list_button_discard'),
"should have a visible discard button");
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
@@ -227,6 +229,8 @@ QUnit.module('Views', {
"should not have a visible save button");
assert.isNotVisible(list.$buttons.find('.o_list_button_discard'),
"should not have a visible discard button");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
list.destroy();
});
@@ -1114,6 +1118,49 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('head selector is toggled by the other selectors', async function (assert) {
+ assert.expect(6);
+
+ const list = await createView({
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ data: this.data,
+ groupBy: ['bar'],
+ model: 'foo',
+ View: ListView,
+ });
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('.o_group_header:first()'));
+ await testUtils.dom.click(list.$('thead .o_list_record_selector input'));
+
+ assert.containsN(list, 'tbody .o_list_record_selector input:checked',
+ 3, "All visible checkboxes should be checked");
+
+ await testUtils.dom.click(list.$('.o_group_header:last()'));
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('tbody .o_list_record_selector input:last()'));
+
+ assert.ok(list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be checked");
+
+ await testUtils.dom.click(list.$('tbody .o_list_record_selector:first() input'));
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('.o_group_header:first()'));
+
+ assert.ok(list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be checked");
+
+ list.destroy();
+ });
+
QUnit.test('selection is reset on reload', async function (assert) {
assert.expect(5);
@@ -2889,7 +2936,7 @@ QUnit.module('Views', {
});
QUnit.test('edition: create new line, then discard', async function (assert) {
- assert.expect(8);
+ assert.expect(11);
var list = await createView({
View: ListView,
@@ -2904,11 +2951,13 @@ QUnit.module('Views', {
"create button should be visible");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 0,
"create button should be hidden");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 1,
"discard button should be visible");
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
assert.containsN(list, 'tr.o_data_row', 4,
"should still have 4 records");
@@ -2916,6 +2965,8 @@ QUnit.module('Views', {
"create button should be visible again");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden again");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
list.destroy();
});
@@ -4500,8 +4551,34 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list view: single edition with selected records', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `<tree editable="top"><field name="foo"/></tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // Select first record
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+
+ // Edit the second
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:first()'));
+ await testUtils.fields.editInput(list.$('.o_data_row:eq(1) .o_data_cell:first() input'), "oui");
+ await testUtils.dom.click($('.o_list_button_save'));
+
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell:first()').text(), "yop",
+ "First row should remain unchanged");
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell:first()').text(), "oui",
+ "Second row should have been updated");
+
+ list.destroy();
+ });
+
QUnit.test('editable list view: multi edition', async function (assert) {
- assert.expect(19);
+ assert.expect(24);
var list = await createView({
View: ListView,
@@ -4544,7 +4621,7 @@ QUnit.module('Views', {
assert.verifySteps(['default_get']);
await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), 123);
- assert.containsNone($, '.modal', "the multi edition should not be triggered during creation");
+ assert.containsNone(document.body, '.modal', "the multi edition should not be triggered during creation");
await testUtils.dom.click($('.o_list_button_save'));
assert.verifySteps(['create', 'read']);
@@ -4552,15 +4629,241 @@ QUnit.module('Views', {
// edit a field
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
- assert.containsOnce($, '.modal', "there should be an opened modal");
- assert.ok($('.modal').text().includes('2 valid'), "the number of records should be correctly displayed");
-
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ assert.containsOnce(document.body, '.modal', "modal appears when switching cells");
+ await testUtils.dom.click($('.modal .btn:contains(Cancel)'));
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10',
+ "changes have been discarded and row is back to readonly");
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1)')[0],
+ "focus should be given to the most recently edited cell after discard");
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:eq(0)'));
+ assert.ok($('.modal').text().includes('2 selected records'), "the number of records should be correctly displayed");
await testUtils.dom.click($('.modal .btn-primary'));
+ assert.containsNone(list, '.o_data_cell input.o_field_widget', "no field should be editable anymore");
assert.verifySteps(['write', 'read']);
assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), "yop666",
"the first row should be updated");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "blip666",
"the second row should be updated");
+ assert.containsNone(list, '.o_data_cell input.o_field_widget', "no field should be editable anymore");
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1)')[0],
+ "focus should be given to the most recently edited cell after confirm");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition and onchanges', async function (assert) {
+ assert.expect(10);
+
+ this.data.foo.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length;
+ },
+ };
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'write') {
+ args.args[1].int_field = args.args[1].foo.length;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit a single record to validate that onchanges work
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=foo]'), 'hi');
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ await testUtils.dom.click(list.$('.o_list_button_discard'));
+
+ assert.containsOnce(document.body, '.modal'); // discard dialog
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.verifySteps(['/web/dataset/search_read', 'onchange']);
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ // edit foo, first row
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=foo]'), 'hello');
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+
+ assert.containsOnce(document.body, '.modal'); // save dialog
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), "hello5");
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "hello5");
+
+ assert.verifySteps(['write', 'read'], "should not perform the onchange in multi edition");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition error and cancellation handling', async function (assert) {
+ assert.expect(12);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo" required="1"/>' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ });
+
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ // edit a line and cancel
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
+ await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=foo]'), "abc");
+ await testUtils.dom.click($('.modal .btn:contains("Cancel")'));
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "first cell should have discarded any change");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ // edit a line with an invalid format type
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
+ await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), "hahaha");
+ assert.containsOnce(document.body, '.modal', "there should be an opened modal");
+ await testUtils.dom.click($('.modal .btn-primary'));
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "changes should be discarded");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ // edit a line with an invalid value
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
+ await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=foo]'), "");
+ assert.containsOnce(document.body, '.modal', "there should be an opened modal");
+ await testUtils.dom.click($('.modal .btn-primary'));
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "changes should be discarded");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition of many2one: set same value', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="m2o"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args, [[1, 2, 3, 4], { m2o: 1 }],
+ "should force write value on all selected records");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(list.$('.o_list_many2one').text(), "Value 1Value 2Value 1Value 1");
+
+ // select all records (the first one has value 1 for m2o)
+ await testUtils.dom.click(list.$('thead .o_list_record_selector input'));
+
+ // set m2o to 1 in first record
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ await testUtils.fields.many2one.searchAndClickItem('m2o', {search: 'Value 1'});
+
+ assert.containsOnce(document.body, '.modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_list_many2one').text(), "Value 1Value 1Value 1Value 1");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: clicking on "Discard changes" in multi edition', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+ list.$('.o_data_row:first() .o_data_cell:first() input').val("oof");
+
+ const $discardButton = list.$buttons.find('.o_list_button_discard');
+
+ // Simulates an actual click (event chain is: mousedown > change > blur > focus > mouseup > click)
+ await testUtils.dom.triggerEvents($discardButton, ['mousedown']);
+ await testUtils.dom.triggerEvents(list.$('.o_data_row:first() .o_data_cell:first() input'),
+ ['change', 'blur', 'focusout']);
+ await testUtils.dom.triggerEvents($discardButton, ['focus']);
+ $discardButton[0].dispatchEvent(new MouseEvent('mouseup'));
+ await testUtils.dom.click($discardButton);
+
+ assert.ok($('.modal').text().includes("Warning"), "Modal should ask to discard changes");
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first() .o_data_cell:first()').text(), "yop");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view (multi edition): mousedown on "Discard", but mouseup somewhere else', async function (assert) {
+ assert.expect(1);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+ list.$('.o_data_row:first() .o_data_cell:first() input').val("oof");
+
+ // Simulates a pseudo drag and drop
+ await testUtils.dom.triggerEvents(list.$buttons.find('.o_list_button_discard'), ['mousedown']);
+ await testUtils.dom.triggerEvents(list.$('.o_data_row:first() .o_data_cell:first() input'),
+ ['change', 'blur', 'focusout']);
+ await testUtils.dom.triggerEvents($(document.body), ['focus']);
+ window.dispatchEvent(new MouseEvent('mouseup'));
+ await testUtils.nextTick();
+
+ assert.ok($('.modal').text().includes("Confirmation"), "Modal should ask to save changes");
+ await testUtils.dom.click($('.modal .btn-primary'));
list.destroy();
});
@@ -4606,6 +4909,65 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list view: many2one with readonly modifier', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch:
+ `<tree editable="top">
+ <field name="m2o" readonly="1"/>
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // edit a field
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+
+ assert.containsOnce(list, '.o_data_row:eq(0) .o_data_cell:eq(0) a[name="m2o"]');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1) input')[0],
+ "focus should go to the char input");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition: edit and validate last row', async function (assert) {
+ assert.expect(3);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ // in this test, we want to accurately mock what really happens, that is, input
+ // fields only trigger their changes on 'change' event, not on 'input'
+ fieldDebounce: 100000,
+ });
+
+ assert.containsN(list, '.o_data_row', 4);
+
+ // select all records
+ await testUtils.dom.click(list.$('.o_list_view thead .o_list_record_selector input'));
+
+ // edit last cell of last line
+ await testUtils.dom.click(list.$('.o_data_row:last .o_data_cell:last'));
+ testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), '666');
+ await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row .o_data_cell:last input'), 'enter');
+
+ assert.containsOnce(document.body, '.modal');
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.containsN(list, '.o_data_row', 4,
+ "should not create a new row as we were in multi edition");
+
+ list.destroy();
+ });
+
QUnit.test('list grouped by date:month', async function (assert) {
assert.expect(1);
@@ -6325,6 +6687,28 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('add and discard a line through keyboard navigation without crashing', async function (assert) {
+ assert.expect(2);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" required="1"/></tree>',
+ groupBy: ['bar'],
+ });
+
+ await testUtils.dom.click(list.$('.o_group_header:first')); // open group
+ // Triggers ENTER on "Add a line" wrapper cell
+ await testUtils.fields.triggerKeydown(list.$('.o_group_field_row_add'), 'enter');
+ assert.containsN(list, 'tbody:nth(1) .o_data_row', 4, "new data row should be created");
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
+ // At this point, a crash manager should appear if no proper link targetting
+ assert.containsN(list, 'tbody:nth(1) .o_data_row', 3,"new data row should be discarded.");
+
+ list.destroy();
+ });
+
QUnit.test('editable grouped list with create="0"', async function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js
index 88720e77ede..a94b0c4ce8e 100644
--- a/addons/web/static/tests/views/search_view_tests.js
+++ b/addons/web/static/tests/views/search_view_tests.js
@@ -604,6 +604,7 @@ QUnit.module('Search View', {
// DateTime case
$autocomplete = $('.o_searchview_input');
await stringToEvent($autocomplete, '07/15/1983 00:00:00');
+ await testUtils.fields.triggerKey('down', $autocomplete, 'down');
await testUtils.fields.triggerKey('up', $autocomplete, 'enter');
assert.equal($('.o_searchview_facet .o_facet_values').text().trim(), '07/15/1983 00:00:00',
diff --git a/addons/web/static/tests/widgets/domain_selector_tests.js b/addons/web/static/tests/widgets/domain_selector_tests.js
index 305315b72c6..8c246fd8399 100644
--- a/addons/web/static/tests/widgets/domain_selector_tests.js
+++ b/addons/web/static/tests/widgets/domain_selector_tests.js
@@ -174,6 +174,7 @@ QUnit.module('DomainSelector', {
await testUtils.dom.clickFirst($('.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]'));
assert.notEqual(domainSelector.$(".o_datepicker:visible input").val(), val,
"datepicker value should have changed");
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget a[data-action=close]'));
domainSelector.destroy();
});
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index ef2edf98b35..d0bfb83168e 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -341,9 +341,11 @@ table.table_desc tr td {
// Title
.s_title {
.s_title_boxed {
- display: inline-block;
- padding: $grid-gutter-width;
- border: 1px solid;
+ > * {
+ display: inline-block;
+ padding: $grid-gutter-width;
+ border: 1px solid;
+ }
}
.s_title_lines {
overflow: hidden;
diff --git a/addons/website_event_questions/report/report_event_question_view.xml b/addons/website_event_questions/report/report_event_question_view.xml
index f2982ac35b5..109d3414a2c 100644
--- a/addons/website_event_questions/report/report_event_question_view.xml
+++ b/addons/website_event_questions/report/report_event_question_view.xml
@@ -40,6 +40,9 @@
<filter string="Event" name="group_by_event" context="{'group_by': 'event_id'}" />
<filter string="Question" name="group_by_question" context="{'group_by': 'question_id'}"/>
<filter string="Answer" name="group_by_answer" context="{'group_by': 'answer_id'}" />
+ <filter name="exclude_cancelled"
+ string="Exclude cancelled registrations"
+ domain="[('attendee_id.state', '!=', 'cancel')]" />
</group>
</search>
</field>
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml
index 18a1bb70a51..c6314d9bf69 100644
--- a/addons/website_sale/data/mail_template_data.xml
+++ b/addons/website_sale/data/mail_template_data.xml
@@ -38,7 +38,7 @@
<h1 style="color:#A9A9A9;">THERE'S SOMETHING IN YOUR CART.</h1>
Would you like to complete your purchase?<br/><br/>
% if object.order_line:
- % for line in object.order_line:
+ % for line in object.website_order_line:
<hr/>
<table width="100%">
<tr>
diff --git a/addons/website_sale_wishlist/controllers/main.py b/addons/website_sale_wishlist/controllers/main.py
index 09bd5f90b17..b494aeb927d 100644
--- a/addons/website_sale_wishlist/controllers/main.py
+++ b/addons/website_sale_wishlist/controllers/main.py
@@ -55,5 +55,5 @@ class WebsiteSaleWishlist(WebsiteSale):
request.session.modified = True
wish.sudo().unlink()
else:
- wish.active = False
+ wish.unlink()
return True
diff --git a/doc/howtos/website/module-empty b/doc/howtos/website/module-empty
index 88fe8079d92..db169c753aa 100644
--- a/doc/howtos/website/module-empty
+++ b/doc/howtos/website/module-empty
@@ -28,7 +28,7 @@ new file mode 100644
+ """,
+
+ 'author': "My Company",
-+ 'website': "http://www.yourcompany.com",
++ 'website': "https://www.yourcompany.com",
+
+ # Categories can be used to filter modules in modules listing
+ # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
diff --git a/doc/reference/actions.rst b/doc/reference/actions.rst
index e247bde9e61..614c06187e4 100644
--- a/doc/reference/actions.rst
+++ b/doc/reference/actions.rst
@@ -18,19 +18,23 @@ e.g. button methods. All actions share two mandatory attributes:
``name``
short user-readable description of the action, may be displayed in the
client's interface
+``binding_model_id``
+ if set, the action is available in the action bar for the given model
+
+ .. note:: For Server Actions, use ``model_id``.
A client can get actions in 4 forms:
-``False``
- if any action dialog is currently open, close it
-A string
- if a :ref:`client action <reference/actions/client>` matches, interpret as
- a client action's tag, otherwise treat as a number
-A number
- read the corresponding action record from the database, may be a database
- identifier or an :term:`external id`
-A dictionary
- treat as a client action descriptor and execute
+* ``False``
+ if any action dialog is currently open, close it
+* A string
+ if a :ref:`client action <reference/actions/client>` matches, interpret as
+ a client action's tag, otherwise treat as a number
+* A number
+ read the corresponding action record from the database, may be a database
+ identifier or an :term:`external id`
+* A dictionary
+ treat as a client action descriptor and execute
Bindings
========
@@ -122,12 +126,27 @@ dialog::
In-database window actions have a few different fields which should be ignored
by clients, mostly to use in composing the ``views`` list:
-``view_mode``
- comma-separated list of view types as a string. All of these types will be
+``view_mode`` (default= ``tree,form`` )
+ comma-separated list of view types as a string (/!\\ No spaces /!\\). All of these types will be
present in the generated ``views`` list (with at least a ``False`` view_id)
``view_ids``
M2M\ [#notquitem2m]_ to view objects, defines the initial content of
``views``
+
+ .. note:: Act_window views can also be defined cleanly through ``ir.actions.act_window.view``.
+
+ If you plan to allow multiple views for your model, prefer using
+ ir.actions.act_window.view instead of the action ``view_ids``
+
+ .. code-block:: xml
+
+ <record model="ir.actions.act_window.view" id="test_action_tree">
+ <field name="sequence" eval="1"/>
+ <field name="view_mode">tree</field>
+ <field name="view_id" ref="view_test_tree"/>
+ <field name="act_window_id" ref="test_action"/>
+ </record>
+
``view_id``
specific view added to the ``views`` list in case its type is part of the
``view_mode`` list and not already filled by one of the views in
@@ -160,6 +179,9 @@ The server-side composition of the ``views`` sequence is the following:
* ``groups_id``?
* ``filter``?
+.. [#notquitem2m] technically not an M2M: adds a sequence field and may be
+ composed of just a view type, without a view id.
+
.. _reference/actions/url:
URL Actions (``ir.actions.act_url``)
@@ -178,7 +200,7 @@ via two fields:
{
"type": "ir.actions.act_url",
- "url": "http://odoo.com",
+ "url": "https://odoo.com",
"target": "self",
}
@@ -189,6 +211,8 @@ will replace the current content section by the Odoo home page.
Server Actions (``ir.actions.server``)
======================================
+.. autoclass:: odoo.addons.base.models.ir_actions.IrActionsServer
+
Allow triggering complex server code from any valid action location. Only
two fields are relevant to clients:
@@ -202,151 +226,90 @@ specific or generic actions based on their ``state``. Some fields (and
corresponding behaviors) are shared between states:
``model_id``
- Odoo model linked to the action, made available in
- :ref:`evaluation contexts <reference/actions/server/context>`
-``condition`` (optional)
- evaluated as Python code using the server action's
- :ref:`evaluation context <reference/actions/server/context>`. If
- ``False``, prevents the action from running. Default: ``True``
+ Odoo model linked to the action.
-Valid action types (``state`` field) are extensible, the default types are:
+``state``
-``code``
---------
+* ``code``: Executes python code given through the ``code`` argument.
-The default and most flexible server action type, executes arbitrary Python
-code with the action's :ref:`evaluation context
-<reference/actions/server/context>`. Only uses one specific type-specific
-field:
+* ``object_create``: Creates a new record of model ``crud_model_id`` following ``fields_lines`` specifications.
-``code``
- a piece of Python code to execute when the action is called
+* ``object_write``: Updates the current record(s) following ``fields_lines`` specifications
-.. code-block:: xml
+* ``multi``: Executes serveral actions given through the ``child_ids`` argument.
- <record model="ir.actions.server" id="print_instance">
- <field name="name">Res Partner Server Action</field>
- <field name="model_id" ref="model_res_partner"/>
- <field name="code">
- raise Warning(object.name)
- </field>
- </record>
+State fields
+------------
+
+Depending on its state, the behavior is defined through different fields.
+The concerned state is given after each field.
+
+``code`` (code)
+ Specify a piece of Python code to execute when the action is called
+
+ .. code-block:: xml
+
+ <record model="ir.actions.server" id="print_instance">
+ <field name="name">Res Partner Server Action</field>
+ <field name="model_id" ref="model_res_partner"/>
+ <field name="code">
+ raise Warning(record.name)
+ </field>
+ </record>
+
+ .. note::
+
+ The code segment can define a variable called ``action``, which will be
+ returned to the client as the next action to execute:
+
+ .. code-block:: xml
+
+ <record model="ir.actions.server" id="print_instance">
+ <field name="name">Res Partner Server Action</field>
+ <field name="model_id" ref="model_res_partner"/>
+ <field name="code">
+ if record.some_condition():
+ action = {
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": record._name,
+ "res_id": record.id,
+ }
+ </field>
+ </record>
+
+ will ask the client to open a form for the record if it fulfills some
+ condition
+
+ .. This tends to be the only action type created from :ref:`data files
+ <reference/data>`, other types aside from
+ :ref:`reference/actions/server/multi` are simpler than Python code to define
+ from the UI, but not from :ref:`data files <reference/data>`.
+
+``crud_model_id`` (create)(required)
+ model in which to create a new record
+``link_field_id`` (create)
+ many2one to ``ir.model.fields``, specifies the current record's m2o field
+ on which the newly created record should be set (models should match)
-.. note::
-
- The code segment can define a variable called ``action``, which will be
- returned to the client as the next action to execute:
-
- .. code-block:: xml
-
- <record model="ir.actions.server" id="print_instance">
- <field name="name">Res Partner Server Action</field>
- <field name="model_id" ref="model_res_partner"/>
- <field name="code">
- if object.some_condition():
- action = {
- "type": "ir.actions.act_window",
- "view_mode": "form",
- "res_model": object._name,
- "res_id": object.id,
- }
- </field>
- </record>
-
- will ask the client to open a form for the record if it fulfills some
- condition
-
-This tends to be the only action type created from :ref:`data files
-<reference/data>`, other types aside from
-:ref:`reference/actions/server/multi` are simpler than Python code to define
-from the UI, but not from :ref:`data files <reference/data>`.
-
-.. _reference/actions/server/object_create:
-
-``object_create``
------------------
-
-Creates a new record, from scratch (via :meth:`~odoo.models.Model.create`)
-or by copying an existing record (via :meth:`~odoo.models.Model.copy`)
-
-``use_create``
- the creation policy, one of:
-
- ``new``
- creates a record in the model specified by ``model_id``
- ``new_other``
- creates a record in the model specified by ``crud_model_id``
- ``copy_current``
- copies the record on which the action was invoked
- ``copy_other``
- copies an other record, obtained via ``ref_object``
-``fields_lines``
+``fields_lines`` (create/write)
fields to override when creating or copying the record.
:class:`~odoo.fields.One2many` with the fields:
``col1``
- ``ir.model.fields`` to set in the model implied by ``use_create``
+ ``ir.model.fields`` to set in the concerned model
+ (``crud_model_id`` for creates, ``model_id`` for updates)
``value``
value for the field, interpreted via ``type``
- ``type``
+ ``type`` (value|reference|equation)
If ``value``, the ``value`` field is interpreted as a literal value
(possibly converted), if ``equation`` the ``value`` field is
interpreted as a Python expression and evaluated
-``crud_model_id``
- model in which to create a new record, if ``use_create`` is set to
- ``new_other``
-``ref_object``
- :class:`~odoo.fields.Reference` to an arbitrary record to copy, used if
- ``use_create`` is set to ``copy_other``
-``link_new_record``
- boolean flag linking the newly created record to the current one via a
- many2one field specified through ``link_field_id``, defaults to ``False``
-``link_field_id``
- many2one to ``ir.model.fields``, specifies the current record's m2o field
- on which the newly created record should be set (models should match)
-
-``object_write``
-----------------
-
-Similar to :ref:`reference/actions/server/object_create` but alters an
-existing records instead of creating one
-
-``use_write``
- write policy, one of:
-
- ``current``
- write to the current record
- ``other``
- write to an other record selected via ``crud_model_id`` and
- ``ref_object``
- ``expression``
- write to an other record whose model is selected via ``crud_model_id``
- and whose id is selected by evaluating ``write_expression``
-``write_expression``
- Python expression returning a record or an object id, used when
- ``use_write`` is set to ``expression`` in order to decide which record
- should be modified
-``fields_lines``
- see :ref:`reference/actions/server/object_create`
-``crud_model_id``
- see :ref:`reference/actions/server/object_create`
-``ref_object``
- see :ref:`reference/actions/server/object_create`
-
-.. _reference/actions/server/multi:
-
-``multi``
----------
-
-Executes multiple actions one after the other. Actions to execute are defined
-via the ``child_ids`` m2m. If sub-actions themselves return actions, the last
-one will be returned to the client as the multi's own next action
-``client_action``
------------------
-
-Indirection for directly returning an other action defined using
-``action_id``. Simply returns that action to the client for execution.
+``child_ids`` (multi)
+ Specify the multiple sub-actions (``ir.actions.server``) to enact in state multi.
+ If sub-actions themselves return actions, the last
+ one will be returned to the client as the multi's own next action
.. _reference/actions/server/context:
@@ -356,23 +319,12 @@ Evaluation context
A number of keys are available in the evaluation context of or surrounding
server actions:
-``model``
- the model object linked to the action via ``model_id``
-``object``, ``obj``
- only available if ``active_model`` and ``active_id`` are provided (via
- context) otherwise ``None``. The actual record selected by ``active_id``
-``pool``
- the current database registry
-``datetime``, ``dateutil``, ``time``
- corresponding Python modules
-``cr``
- the current cursor
-``user``
- the current user record
-``context``
- execution context
-``Warning``
- constructor for the ``Warning`` exception
+* ``model`` model object linked to the action via ``model_id``
+* ``record``/``records`` record/recorset on which the action is triggered, can be void.
+* ``env`` Odoo Environment
+* ``datetime``, ``dateutil``, ``time``, ``timezone`` corresponding Python modules
+* ``log: log(message, level='info')`` logging function to record debug information in ir.logging table
+* ``Warning`` constructor for the ``Warning`` exception
.. _reference/actions/report:
@@ -386,13 +338,15 @@ Triggers the printing of a report
in a list of some sort
``model`` (mandatory)
the model your report will be about
-``report_type`` (mandatory)
+``report_type`` (default=qweb-pdf)
either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML
-``report_name``
+``report_name`` (mandatory)
the name of your report (which will be the name of the PDF output)
``groups_id``
:class:`~odoo.fields.Many2many` field to the groups allowed to view/use
the current report
+``multi``
+ if set to ``True``, the action will not be displayed on a form view.
``paperformat_id``
:class:`~odoo.fields.Many2one` field to the paper format you wish to
use for this report (if not specified, the company format will be used)
@@ -436,5 +390,40 @@ Triggers an action implemented entirely in the client.
tells the client to start the Point of Sale interface, the server has no idea
how the POS interface works.
-.. [#notquitem2m] technically not an M2M: adds a sequence field and may be
- composed of just a view type, without a view id.
+.. _reference/actions/cron:
+
+Automated Actions (``ir.cron``)
+======================================
+
+Actions triggered automatically on a predefined frequency.
+
+``name``
+ Name of the automated action (Mainly used in log display)
+
+``interval_number``
+ Number of *interval_type* uom between two executions of the action
+
+``interval_type``
+ Unit of measure of frequency interval (``minutes``, ``hours``, ``days``, ``weeks``, ``months``,
+
+``numbercall``
+ Number of times this action has to be run.
+ If the action is expected to run indefinitely, set to ``-1``.
+
+``doall``
+ Boolean precising whether the missed actions have to be executed in case of
+ server restarts.
+
+``model_id``
+ Model on which this action will be called
+
+``code``
+ Code content of the action.
+ Can be a simple call to the model's method :
+
+ .. code-block:: python
+
+ model.<method_name>()
+
+``nextcall``
+ Next planned execution date of this action (date/time format)
diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst
index d10a3e8c0ec..e954a5922df 100644
--- a/doc/reference/cmdline.rst
+++ b/doc/reference/cmdline.rst
@@ -19,6 +19,8 @@ Running the server
Providing a comma-separated list restrict access to databases provided in
list.
+ For advanced database options, take a look :ref:`below <reference/cmdline/server/database>`.
+
.. option:: -i <modules>, --init <modules>
comma-separated list of modules to install before running the server
@@ -32,82 +34,24 @@ Running the server
.. option:: --addons-path <directories>
comma-separated list of directories in which modules are stored. These
- directories are scanned for modules (nb: when and why?)
-
-.. option:: --workers <count>
-
- if ``count`` is not 0 (the default), enables multiprocessing and sets up
- the specified number of HTTP workers (sub-processes processing HTTP
- and RPC requests).
-
- .. note:: multiprocessing mode is only available on Unix-based systems
-
- A number of options allow limiting and recycling workers:
-
- .. option:: --limit-request <limit>
-
- Number of requests a worker will process before being recycled and
- restarted.
-
- Defaults to 8196.
-
- .. option:: --limit-memory-soft <limit>
-
- Maximum allowed virtual memory per worker. If the limit is exceeded,
- the worker is killed and recycled at the end of the current request.
-
- Defaults to 2048MiB.
-
- .. option:: --limit-memory-hard <limit>
-
- Hard limit on virtual memory, any worker exceeding the limit will be
- immediately killed without waiting for the end of the current request
- processing.
-
- Defaults to 2560MiB.
-
- .. option:: --limit-time-cpu <limit>
-
- Prevents the worker from using more than <limit> CPU seconds for each
- request. If the limit is exceeded, the worker is killed.
-
- Defaults to 60.
-
- .. option:: --limit-time-real <limit>
-
- Prevents the worker from taking longer than <limit> seconds to process
- a request. If the limit is exceeded, the worker is killed.
-
- Differs from :option:`--limit-time-cpu` in that this is a "wall time"
- limit including e.g. SQL queries.
+ directories are scanned for modules.
- Defaults to 120.
-
-.. option:: --max-cron-threads <count>
-
- number of workers dedicated to cron jobs. Defaults to 2. The workers are
- threads in multi-threading mode and processes in multi-processing mode.
-
- For multi-processing mode, this is in addition to the HTTP worker
- processes.
+ .. (nb: when and why?)
.. option:: -c <config>, --config <config>
- provide an alternate configuration file
+ provide an alternate :ref:`configuration file <reference/cmdline/config>`
.. option:: -s, --save
saves the server configuration to the current configuration file
(:file:`{$HOME}/.odoorc` by default, and can be overridden using
- :option:`-c`)
-
-.. option:: --proxy-mode
+ :option:`-c`).
- enables the use of ``X-Forwarded-*`` headers through `Werkzeug's proxy
- support`_.
+.. option:: --without-demo
- .. warning:: proxy mode *must not* be enabled outside of a reverse proxy
- scenario
+ disables demo data loading for modules installed
+ comma-separated, use ``all`` for all modules.
.. option:: --test-enable
@@ -117,25 +61,9 @@ Running the server
select the tests to run by using tags.
-.. option:: --dev <feature,feature,...,feature>
-
- * ``all``: all the features below are activated
-
- * ``xml``: read template qweb from xml file directly instead of database.
- Once a template has been modified in database, it will be not be read from
- the xml file until the next update/init.
-
- * ``reload``: restart server when python file are updated (may not be detected
- depending on the text editor used)
-
- * ``qweb``: break in the evaluation of qweb template when a node contains ``t-debug='debugger'``
-
- * ``(i)p(u)db``: start the chosen python debugger in the code when an
- unexpected error is raised before logging and returning the error.
-
.. _reference/cmdline/server/database:
-database
+Database
--------
.. option:: -r <user>, --db_user <user>
@@ -175,40 +103,40 @@ database
listen by using the --database parameter and specifying a comma-separated
list of databases
- When combining the two parameters, db-filter superseed the comma-separated
+ When combining the two parameters, db-filter supersedes the comma-separated
database list for restricting database list, while the comma-separated list
is used for performing requested operations like upgrade of modules.
-
+
.. code-block:: bash
- odoo-bin --db-filter ^11.*$
+ $ odoo-bin --db-filter ^11.*$
Restrict access to databases whose name starts with 11
.. code-block:: bash
- odoo-bin --database 11firstdatabase,11seconddatabase
+ $ odoo-bin --database 11firstdatabase,11seconddatabase
Restrict access to only two databases, 11firstdatabase and 11seconddatabase
-
+
.. code-block:: bash
- odoo-bin --database 11firstdatabase,11seconddatabase -u base
+ $ odoo-bin --database 11firstdatabase,11seconddatabase -u base
Restrict access to only two databases, 11firstdatabase and 11seconddatabase,
- and update base module on one database: 11firstdatabase
+ and update base module on one database: 11firstdatabase.
If database 11seconddatabase doesn't exist, the database is created and base modules
is installed
-
+
.. code-block:: bash
- odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
-
+ $ odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
+
Restrict access to databases whose name starts with 11,
- and update base module on one database: 11firstdatabase
+ and update base module on one database: 11firstdatabase.
If database 11seconddatabase doesn't exist, the database is created and base modules
is installed
-
+
.. option:: --db-template <template>
when creating new databases from the database-management screens, use the
@@ -223,7 +151,7 @@ database
.. option:: --no-database-list
Suppresses the ability to list databases available on the system
-
+
.. option:: --db_sslmode
Control the SSL security of the connection between Odoo and PostgreSQL.
@@ -231,6 +159,33 @@ database
'verify-ca' or 'verify-full'
Default value is 'prefer'
+.. _reference/cmdline/server/emails:
+
+Emails
+------
+
+.. option:: --email-from <address>
+
+ Email address used as <FROM> when Odoo needs to send mails
+
+.. option:: --smtp <server>
+
+ Address of the SMTP server to connect to in order to send mails
+
+.. option:: --smtp-port <port>
+
+.. option:: --smtp-ssl
+
+ If set, odoo should use SSL/STARTSSL SMTP connections
+
+.. option:: --smtp-user <name>
+
+ Username to connect to the SMTP server
+
+.. option:: --smtp-password <password>
+
+ Password to connect to the SMTP server
+
.. _reference/cmdline/server/internationalisation:
Internationalisation
@@ -269,13 +224,41 @@ of importation
specify modules to export. Use in combination with --i18n-export
+.. _reference/cmdline/advanced:
+
+Advanced Options
+----------------
+
+.. _reference/cmdline/dev:
+
+Developer features
+''''''''''''''''''
+
+.. option:: --dev <feature,feature,...,feature>
+
+ * ``all``: all the features below are activated
+
+ * ``xml``: read template qweb from xml file directly instead of database.
+ Once a template has been modified in database, it will be not be read from
+ the xml file until the next update/init.
+
+ * ``reload``: restart server when python file are updated (may not be detected
+ depending on the text editor used)
+
+ * ``qweb``: break in the evaluation of qweb template when a node contains ``t-debug='debugger'``
+
+ * ``(i)p(u)db``: start the chosen python debugger in the code when an
+ unexpected error is raised before logging and returning the error.
+
-built-in HTTP
--------------
+.. _reference/cmdline/server/http:
+
+HTTP
+''''
.. option:: --no-http
- do not start the HTTP or long-polling workers (may still start cron
+ do not start the HTTP or long-polling workers (may still start :ref:`cron <reference/actions/cron>`
workers)
.. warning:: has no effect if :option:`--test-enable` is set, as tests
@@ -295,25 +278,35 @@ built-in HTTP
TCP port for long-polling connections in multiprocessing or gevent mode,
defaults to 8072. Not used in default (threaded) mode.
-logging
--------
+.. option:: --proxy-mode
+
+ enables the use of ``X-Forwarded-*`` headers through `Werkzeug's proxy
+ support`_.
+
+ .. warning:: proxy mode *must not* be enabled outside of a reverse proxy
+ scenario
+
+.. _reference/cmdline/server/logging:
+
+Logging
+'''''''
By default, Odoo displays all logging of level_ ``info`` except for workflow
logging (``warning`` only), and log output is sent to ``stdout``. Various
options are available to redirect logging to other destinations and to
-customize the amount of logging output
+customize the amount of logging output.
.. option:: --logfile <file>
sends logging output to the specified file instead of stdout. On Unix, the
file `can be managed by external log rotation programs
- <https://docs.python.org/2/library/logging.handlers.html#watchedfilehandler>`_
+ <https://docs.python.org/3/library/logging.handlers.html#watchedfilehandler>`_
and will automatically be reopened when replaced
.. option:: --syslog
- logs to the system's event logger: `syslog on unices <https://docs.python.org/2/library/logging.handlers.html#sysloghandler>`_
- and `the Event Log on Windows <https://docs.python.org/2/library/logging.handlers.html#nteventloghandler>`_.
+ logs to the system's event logger: `syslog on unices <https://docs.python.org/3/library/logging.handlers.html#sysloghandler>`_
+ and `the Event Log on Windows <https://docs.python.org/3/library/logging.handlers.html#nteventloghandler>`_.
Neither is configurable
@@ -321,7 +314,7 @@ customize the amount of logging output
logs to the ``ir.logging`` model (``ir_logging`` table) of the specified
database. The database can be the name of a database in the "current"
- PostgreSQL, or `a PostgreSQL URI`_ for e.g. log aggregation
+ PostgreSQL, or `a PostgreSQL URI`_ for e.g. log aggregation.
.. option:: --log-handler <handler-spec>
@@ -388,60 +381,66 @@ customize the amount of logging output
In case of conflict between :option:`--log-level` and
:option:`--log-handler`, the latter is used
-emails
-------
+.. _reference/cdmline/workers:
-.. option:: --email-from <address>
+Multiprocessing
+'''''''''''''''
- Email address used as <FROM> when Odoo needs to send mails
+.. option:: --workers <count>
-.. option:: --smtp <server>
+ if ``count`` is not 0 (the default), enables multiprocessing and sets up
+ the specified number of HTTP workers (sub-processes processing HTTP
+ and RPC requests).
- Address of the SMTP server to connect to in order to send mails
+ .. note:: multiprocessing mode is only available on Unix-based systems
-.. option:: --smtp-port <port>
+ A number of options allow limiting and recycling workers:
-.. option:: --smtp-ssl
+ .. option:: --limit-request <limit>
- If set, odoo should use SSL/STARTSSL SMTP connections
+ Number of requests a worker will process before being recycled and
+ restarted.
-.. option:: --smtp-user <name>
+ Defaults to *8196*.
- Username to connect to the SMTP server
+ .. option:: --limit-memory-soft <limit>
-.. option:: --smtp-password <password>
+ Maximum allowed virtual memory per worker. If the limit is exceeded,
+ the worker is killed and recycled at the end of the current request.
- Password to connect to the SMTP server
+ Defaults to *2048MiB*.
+ .. option:: --limit-memory-hard <limit>
-.. _reference/cmdline/scaffold:
+ Hard limit on virtual memory, any worker exceeding the limit will be
+ immediately killed without waiting for the end of the current request
+ processing.
-Scaffolding
-===========
+ Defaults to *2560MiB*.
-.. program:: odoo-bin scaffold
+ .. option:: --limit-time-cpu <limit>
-Scaffolding is the automated creation of a skeleton structure to simplify
-bootstrapping (of new modules, in the case of Odoo). While not necessary it
-avoids the tedium of setting up basic structures and looking up what all
-starting requirements are.
+ Prevents the worker from using more than <limit> CPU seconds for each
+ request. If the limit is exceeded, the worker is killed.
-Scaffolding is available via the :command:`odoo-bin scaffold` subcommand.
+ Defaults to *60*.
-.. option:: -t <template>
+ .. option:: --limit-time-real <limit>
- a template directory, files are passed through jinja2_ then copied to
- the ``destination`` directory
+ Prevents the worker from taking longer than <limit> seconds to process
+ a request. If the limit is exceeded, the worker is killed.
-.. option:: name
+ Differs from :option:`--limit-time-cpu` in that this is a "wall time"
+ limit including e.g. SQL queries.
- the name of the module to create, may munged in various manners to
- generate programmatic names (e.g. module directory name, model names, …)
+ Defaults to *120*.
+
+.. option:: --max-cron-threads <count>
-.. option:: destination
+ number of workers dedicated to :ref:`cron <reference/actions/cron>` jobs. Defaults to *2*.
+ The workers are threads in multi-threading mode and processes in multi-processing mode.
- directory in which to create the new module, defaults to the current
- directory
+ For multi-processing mode, this is in addition to the HTTP worker processes.
.. _reference/cmdline/config:
@@ -473,15 +472,67 @@ can be overridden using :option:`--config <odoo-bin -c>`. Specifying
to that file.
.. _jinja2: http://jinja.pocoo.org
-.. _regular expression: https://docs.python.org/2/library/re.html
+.. _regular expression: https://docs.python.org/3/library/re.html
.. _password authentication:
- http://www.postgresql.org/docs/9.3/static/auth-methods.html#AUTH-PASSWORD
+ https://www.postgresql.org/docs/9.3/static/auth-methods.html#AUTH-PASSWORD
.. _template database:
- http://www.postgresql.org/docs/9.3/static/manage-ag-templatedbs.html
+ https://www.postgresql.org/docs/9.3/static/manage-ag-templatedbs.html
.. _level:
- https://docs.python.org/2/library/logging.html#logging.Logger.setLevel
+ https://docs.python.org/3/library/logging.html#logging.Logger.setLevel
.. _a PostgreSQL URI:
- http://www.postgresql.org/docs/9.2/static/libpq-connect.html#AEN38208
+ https://www.postgresql.org/docs/9.2/static/libpq-connect.html#AEN38208
.. _Werkzeug's proxy support:
http://werkzeug.pocoo.org/docs/contrib/fixers/#werkzeug.contrib.fixers.ProxyFix
.. _pyinotify: https://github.com/seb-m/pyinotify/wiki
+
+
+Shell
+=====
+
+Odoo command-line also allows to launch odoo as a python console environment.
+This enables direct interaction with the :ref:`orm <reference/orm>` and its functionalities.
+
+
+.. code-block:: console
+
+ $ odoo_bin shell
+
+.. option:: --shell-interface (ipython|ptpython|bpython|python)
+
+ Specify a preferred REPL to use in shell mode.
+
+
+.. _reference/cmdline/scaffold:
+
+Scaffolding
+===========
+
+.. program:: odoo-bin scaffold
+
+Scaffolding is the automated creation of a skeleton structure to simplify
+bootstrapping (of new modules, in the case of Odoo). While not necessary it
+avoids the tedium of setting up basic structures and looking up what all
+starting requirements are.
+
+Scaffolding is available via the :command:`odoo-bin scaffold` subcommand.
+
+.. option:: name (required)
+
+ the name of the module to create, may munged in various manners to
+ generate programmatic names (e.g. module directory name, model names, …)
+
+.. option:: destination (default=current directory)
+
+ directory in which to create the new module, defaults to the current
+ directory
+
+.. option:: -t <template>
+
+ a template directory, files are passed through jinja2_ then copied to
+ the ``destination`` directory
+
+.. code-block:: console
+
+ $ odoo_bin scaffold my_module /addons/
+
+This will create module *my_module* in directory */addons/*. \ No newline at end of file
diff --git a/doc/reference/data.rst b/doc/reference/data.rst
index 2165a490dd0..416121b8255 100644
--- a/doc/reference/data.rst
+++ b/doc/reference/data.rst
@@ -22,6 +22,7 @@ of an XML data file is the following:
.. code-block:: xml
<!-- the root elements of the data file -->
+ <?xml version="1.0" encoding="UTF-8"?>
<odoo>
<operation/>
...
@@ -30,6 +31,25 @@ of an XML data file is the following:
Data files are executed sequentially, operations can only refer to the result
of operations defined previously
+.. note::
+
+ If the content of the data file is expected to be applied only once, you
+ can specify the odoo flag ``noupdate`` set to 1. If part of
+ the data in the file is expected to be applied once, you can place this part
+ of the file in a <data noupdate="1"> domain.
+
+ .. code-block:: xml
+
+ <odoo>
+ <data noupdate="1">
+ <!-- Only loaded when installing the module (odoo-bin -i module) -->
+ <operation/>
+ </data>
+
+ <!-- (Re)Loaded at install and update (odoo-bin -i/-u) -->
+ <operation/>
+ </odoo>
+
Core operations
===============
@@ -152,8 +172,31 @@ Parameters can be provided using ``eval`` (should evaluate to a sequence of
parameters to call the method with) or ``value`` elements (see ``list``
values).
+.. code-block:: xml
+
+ <odoo>
+ <data noupdate="1">
+ <record name="partner_1" model="res.partner">
+ <field name="name">Odude</field>
+ </record>
+
+ <function model="res.partner" name="send_inscription_notice"
+ eval="[[ref('partner_1'), ref('partner_2')]]"/>
+
+ <function model="res.users" name="send_vip_inscription_notice">
+ <function eval="[[('vip','=',True)]]" model="res.partner" name="search"/>
+ </function>
+ </data>
+
+ <record id="model_form_view" model="ir.ui.view">
+
+ </record>
+ </odoo>
+
.. ignored assert
+.. _reference/data/shortcuts:
+
Shortcuts
=========
@@ -166,7 +209,7 @@ data files provide shorter alternatives to defining them using
Defines an ``ir.ui.menu`` record with a number of defaults and fallbacks:
-Parent menu
+``parent``
* If a ``parent`` attribute is set, it should be the :term:`external id`
of an other menu item, used as the new item's parent
* If no ``parent`` is provided, tries to interpret the ``name`` attribute
@@ -175,10 +218,10 @@ Parent menu
created
* Otherwise the menu is defined as a "top-level" menu item (*not* a menu
with no parent)
-Menu name
+``name``
If no ``name`` attribute is specified, tries to get the menu name from
a linked action if any. Otherwise uses the record's ``id``
-Groups
+``groups``
A ``groups`` attribute is interpreted as a comma-separated sequence of
:term:`external identifiers` for ``res.groups`` models. If an
:term:`external identifier` is prefixed with a minus (``-``), the group
@@ -218,7 +261,7 @@ section of the view, and allowing a few *optional* attributes:
``report``
----------
-Creates a ``ir.actions.report`` record with a few default values.
+Creates a :ref:`IrActionsReport <reference/actions/report>` record with a few default values.
Mostly just proxies attributes to the corresponding fields on
``ir.actions.report``, but also automatically creates the item in the
@@ -263,5 +306,5 @@ For each row (record):
* the third column is the ``name`` field for ``res.country.state``
* the fourth column is the ``code`` field for ``res.country.state``
-.. _base64: http://tools.ietf.org/html/rfc3548.html#section-3
-.. _csv: http://en.wikipedia.org/wiki/Comma-separated_values
+.. _base64: https://tools.ietf.org/html/rfc3548.html#section-3
+.. _csv: https://en.wikipedia.org/wiki/Comma-separated_values
diff --git a/doc/reference/javascript_api.rst b/doc/reference/javascript_api.rst
index c47f19b4a9a..b35cd431d5b 100644
--- a/doc/reference/javascript_api.rst
+++ b/doc/reference/javascript_api.rst
@@ -1,12 +1,15 @@
+:orphan:
:banner: banners/javascript.jpg
.. highlight:: javascript
.. default-domain:: js
+.. _api/js:
+
==============
Javascript API
==============
.. automodule:: *
- :members: \ No newline at end of file
+ :members:
diff --git a/doc/reference/javascript_cheatsheet.rst b/doc/reference/javascript_cheatsheet.rst
index 97f7ba5cb5c..b6c5251c46c 100644
--- a/doc/reference/javascript_cheatsheet.rst
+++ b/doc/reference/javascript_cheatsheet.rst
@@ -1,5 +1,7 @@
:banner: banners/javascript.jpg
+.. _reference/jscs:
+
=====================
Javascript Cheatsheet
=====================
@@ -280,6 +282,7 @@ using the view in the kanban arch (a specific example is the helpdesk dashboard)
Promises and asynchronous code
===============================
+
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md
Creating new Promises
@@ -418,6 +421,7 @@ Waiting for Promises
Error handling
--------------
+
- in general in promises
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
@@ -540,4 +544,4 @@ Testing asynchronous code
});
});
- as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write. \ No newline at end of file
+ as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 07434a22e76..fcb1013a3dd 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -15,13 +15,8 @@ description into a live application, able to interact with every model and
records in the database. It is even possible to use the web client to modify
the interface of the web client.
-.. note:: An html version of all docstrings in Odoo is available at:
-
- .. toctree::
- :maxdepth: 2
-
- javascript_api
-
+.. note:: An html version of all js docstrings in Odoo is available at:
+ :ref:`JS API <api/js>`
Overview
=========
@@ -1421,7 +1416,7 @@ the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes
.. raw:: html
<svg width="550" height="173">
- <!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
+ <!-- Created with Method Draw - https://github.com/duopixel/Method-Draw/ -->
<path id="svg_1" d="m147.42498,79.79206c0.09944,-8.18859 -0.06363,-16.38812 0.81774,-24.5623c21.65679,2.68895 43.05815,7.08874 64.35,11.04543c1.14304,-4.01519 0.60504,-7.34585 1.59817,-11.05817c13.67878,7.81176 27.23421,15.73476 40.23409,24.03505c-12.47212,9.41539 -26.77809,17.592 -40.82272,25.96494c-0.4548,-3.89916 -0.90967,-7.79828 -1.36448,-11.69744c-20.69972,3.77225 -42.59036,7.6724 -63.42391,11.12096c-1.41678,-7.95741 -1.37514,-16.62327 -1.38888,-24.84846z" stroke-width="1.5" stroke="#000" fill="#fff"/>
<rect id="svg_3" height="41" width="110" y="57.5" x="7" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#000" fill="#fff"/>
<rect stroke="#000" id="svg_5" height="41" width="135" y="20.5" x="328" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="#fff"/>
@@ -1467,6 +1462,8 @@ the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes
context of a view manager/action manager. They could be used in a client action,
or, they could be displayed in the public website (with some work on the assets).
+.. _reference/js/widgets:
+
Field Widgets
=============
@@ -1910,7 +1907,6 @@ order.
Options:
- - title: title of the bar, displayed on top of the bar options
- editable: boolean if value is editable
- current_value: get the current_value from the field that must be present in the view
- max_value: get the max_value from the field that must be present in the view
@@ -2261,18 +2257,18 @@ do that, several steps should be done.
For more information, look into the *control_panel.js* file.
.. _.appendTo():
- http://api.jquery.com/appendTo/
+ https://api.jquery.com/appendTo/
.. _.prependTo():
- http://api.jquery.com/prependTo/
+ https://api.jquery.com/prependTo/
.. _.insertAfter():
- http://api.jquery.com/insertAfter/
+ https://api.jquery.com/insertAfter/
.. _.insertBefore():
- http://api.jquery.com/insertBefore/
+ https://api.jquery.com/insertBefore/
.. _event delegation:
- http://api.jquery.com/delegate/
+ https://api.jquery.com/delegate/
.. _datepicker: https://github.com/Eonasdan/bootstrap-datetimepicker
diff --git a/doc/reference/mixins.rst b/doc/reference/mixins.rst
index 2f7197a98f2..51525365940 100644
--- a/doc/reference/mixins.rst
+++ b/doc/reference/mixins.rst
@@ -39,11 +39,11 @@ widgets) to your form view will get you up and running in no time.
_name = 'business.trip'
_inherit = ['mail.thread']
_description = 'Business Trip'
-
+
name = fields.Char()
partner_id = fields.Many2one('res.partner', 'Responsible')
guest_ids = fields.Many2many('res.partner', 'Participants')
-
+
In the form view:
.. code-block:: xml
@@ -78,10 +78,10 @@ to manage followers on your record:
.. rubric:: Posting messages
.. method:: message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, **kwargs)
-
+
Post a new message in an existing thread, returning the new
mail.message ID.
-
+
:param str body: body of the message, usually raw HTML that will
be sanitized
:param str message_type: see mail_message.message_type field
@@ -95,20 +95,20 @@ to manage followers on your record:
:rtype: int
.. method:: message_post_with_view(views_or_xmlid, **kwargs):
-
+
Helper method to send a mail / post a message using a view_id to
render using the ir.qweb engine. This method is stand alone, because
there is nothing in template and composer that allows to handle
views in batch. This method will probably disappear when templates
handle ir ui views.
-
+
:param str or ``ir.ui.view`` record: external id or record of the view that
should be sent
.. method:: message_post_with_template(template_id, **kwargs)
-
+
Helper method to send a mail with a template
-
+
:param template_id: the id of the template to render to create the body of the message
:param `\**kwargs`: parameter to create a mail.compose.message wizzard (which inherit from mail.message)
@@ -121,7 +121,7 @@ on the thread's record depending on some values from the email itself (i.e. upda
a date or an e-mail address, add CC's addresses as followers, etc.).
.. method:: message_new(msg_dict, custom_values=None)
-
+
Called by ``message_process`` when a new message is received
for a given thread model, if the message did not belong to
an existing thread.
@@ -129,7 +129,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
The default behavior is to create a new record of the corresponding
model (based on some very basic info extracted from the message).
Additional behavior may be implemented by overriding this method.
-
+
:param dict msg_dict: a map containing the email details and
attachments. See ``message_process`` and ``mail.message.parse`` for details
:param dict custom_values: optional dictionary of additional
@@ -140,14 +140,14 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
:return: the id of the newly created thread object
.. method:: message_update(msg_dict, update_vals=None)
-
+
Called by ``message_process`` when a new message is received
for an existing thread. The default behavior is to update the record
with ``update_vals`` taken from the incoming email.
-
+
Additional behavior may be implemented by overriding this
method.
-
+
:param dict msg_dict: a map containing the email details and attachments;
see ``message_process`` and ``mail.message.parse()`` for details.
:param dict update_vals: a dict containing values to update records given
@@ -159,7 +159,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
.. method:: message_subscribe(partner_ids=None, channel_ids=None, subtype_ids=None, force=True)
Add partners to the records followers.
-
+
:param list(int) partner_ids: IDs of the partners that will be subscribed
to the record
:param list(int) channel_ids: IDs of the channels that will be subscribed
@@ -175,7 +175,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
.. method:: message_unsubscribe(partner_ids=None, channel_ids=None)
Remove partners from the record's followers.
-
+
:param list(int) partner_ids: IDs of the partners that will be subscribed
to the record
:param list(int) channel_ids: IDs of the channels that will be subscribed
@@ -211,12 +211,12 @@ to a field, simple set the tracking attribute to True.
_name = 'business.trip'
_inherit = ['mail.thread']
_description = 'Business Trip'
-
+
name = fields.Char(tracking=True)
partner_id = fields.Many2one('res.partner', 'Responsible',
tracking=True)
guest_ids = fields.Many2many('res.partner', 'Participants')
-
+
From now on, every change to a trip's name or responsible will log a note
on the record. The ``name`` field will be displayed in the notification as
well to give more context about the notification (even if the name did not
@@ -232,32 +232,32 @@ subtype of notifications they wish to receive.
Subtypes are created as data in your module; the model has the following fields:
-``name`` (mandatory) - :class:`~odoo.fields.Char`
+``name`` (mandatory) - :class:`~odoo.fields.Char`
name of the subtype, will be displayed in the notification customization
popup
-``description`` - :class:`~odoo.fields.Char`
+``description`` - :class:`~odoo.fields.Char`
description that will be added in the message posted for this
subtype. If void, the name will be added instead
-``internal`` - :class:`~odoo.fields.Boolean`
+``internal`` - :class:`~odoo.fields.Boolean`
messages with internal subtypes will be visible only by employees,
aka members of the ``base.group_user`` group
-``parent_id`` - :class:`~odoo.fields.Many2one`
+``parent_id`` - :class:`~odoo.fields.Many2one`
link subtypes for automatic subscription; for example project subtypes are
linked to task subtypes through this link. When someone is subscribed to
a project, he will be subscribed to all tasks of this project with
subtypes found using the parent subtype
-``relation_field`` - :class:`~odoo.fields.Char`
+``relation_field`` - :class:`~odoo.fields.Char`
as an example, when linking project and tasks subtypes, the relation
field is the project_id field of tasks
-``res_model`` - :class:`~odoo.fields.Char`
+``res_model`` - :class:`~odoo.fields.Char`
model the subtype applies to; if False, this subtype applies to all models
-``default`` - :class:`~odoo.fields.Boolean`
+``default`` - :class:`~odoo.fields.Boolean`
wether the subtype is activated by default when subscribing
-``sequence`` - :class:`~odoo.fields.Integer`
+``sequence`` - :class:`~odoo.fields.Integer`
used to order subtypes in the notification customization popup
-``hidden`` - :class:`~odoo.fields.Boolean`
+``hidden`` - :class:`~odoo.fields.Boolean`
wether the subtype is hidden in the notification customization popup
-
+
Interfacing subtypes with field tracking allows to subscribe to different kind
of notifications depending on what might interest users. To do this, you
@@ -301,7 +301,7 @@ can override the ``_track_subtype()`` function:
_name = 'business.trip'
_inherit = ['mail.thread']
_description = 'Business Trip'
-
+
name = fields.Char(tracking=True)
partner_id = fields.Many2one('res.partner', 'Responsible',
tracking=True)
@@ -392,7 +392,7 @@ yourself by overriding the function ``_notification_recipients``.
:returns: a subtype's full external id or False if no subtype is triggered
-The urls in the actions list can be generated automatically by calling the
+The urls in the actions list can be generated automatically by calling the
``_notification_link_helper()`` function:
@@ -402,7 +402,7 @@ The urls in the actions list can be generated automatically by calling the
record if the kwargs ``model`` and ``res_id`` are set).
:param str link_type: link type to be generated; can be any of these values:
-
+
``view``
link to form view of the record
``assign``
@@ -418,8 +418,8 @@ The urls in the actions list can be generated automatically by calling the
``new``
open an empty form view for a new record; you can specify
a specific action by providing its id (database id or fully resolved
- external id) in the kwarg ``action_id``
-
+ external id) in the kwarg ``action_id``
+
:returns: link of the type selected for the record
:rtype: str
@@ -428,7 +428,7 @@ The urls in the actions list can be generated automatically by calling the
Let's add a custom button to the Business Trip state change notification;
this button will reset the state to Draft and will be only visible to a member
of the (imaginary) group Travel Manager (``business.group_trip_manager``)
-
+
.. code-block:: python
class BusinessTrip(models.Model):
@@ -437,10 +437,10 @@ The urls in the actions list can be generated automatically by calling the
_description = 'Business Trip'
# Pevious code goes here
-
+
def action_cancel(self):
self.write({'state': 'draft'})
-
+
def _notification_recipients(self, message, groups):
""" Handle Trip Manager recipients that can cancel the trip at the last
minute and kill all the fun. """
@@ -451,7 +451,7 @@ The urls in the actions list can be generated automatically by calling the
app_action = self._notification_link_helper('method',
method='action_cancel')
trip_actions = [{'url': app_action, 'title': _('Cancel')}]
-
+
new_group = (
'group_trip_manager',
lambda partner: bool(partner.user_ids) and
@@ -462,11 +462,11 @@ The urls in the actions list can be generated automatically by calling the
})
return [new_group] + groups
-
- Note that that I could have defined my evaluation function outside of this
+
+ Note that that I could have defined my evaluation function outside of this
method and define a global function to do it instead of a lambda, but for
- the sake of being more brief and less verbose in these documentation files
+ the sake of being more brief and less verbose in these documentation files
that can sometimes be boring, I choose the former instead of the latter.
Overriding defaults
@@ -483,7 +483,7 @@ Context keys:
These context keys can be used to somewhat control ``mail.thread`` features
like auto-subscription or field tracking during calls to ``create()`` or
``write()`` (or any other method where it may be useful).
-
+
- ``mail_create_nosubscribe``: at create or message_post, do not subscribe
the current user to the record thread
- ``mail_create_nolog``: at create, do not log the automatic '<Document>
@@ -496,7 +496,7 @@ Context keys:
- ``mail_notify_force_send``: if less than 50 email notifications to send,
send them directly instead of using the queue; True by default
- ``mail_notify_user_signature``: add the current user signature in
- email notifications; True by default
+ email notifications; True by default
.. _reference/mixins/mail/alias:
@@ -540,14 +540,14 @@ or issues, Sales Team have aliases to generate Leads.
.. note:: The model that will be created by the alias **must** inherit the
``mail_thread`` model.
-
+
Alias support is added by inheriting ``mail.alias.mixin``; this mixin will
generate a new ``mail.alias`` record for each record of the parent class that
gets created (for example, every ``project.project`` record having its ``mail.alias``
record initialized on creation).
.. note:: Aliases can also be created manually and supported by a simple
- :class:`~odoo.fields.Many2one` field. This guide assumes you wish a
+ :class:`~odoo.fields.Many2one` field. This guide assumes you wish a
more complete integration with automatic creation of the alias, record-specific
default values, etc.
@@ -557,25 +557,25 @@ of the created alias, like the kind of record it must create and possibly
some default values these records may have depending on the parent object:
.. method:: get_alias_model_name(vals)
-
+
Return the model name for the alias. Incoming emails that are not
replies to existing records will cause the creation of a new record
of this alias model. The value may depend on ``vals``, the dict of
values passed to ``create`` when a record of this model is created.
-
+
:param vals dict: values of the newly created record that will holding
the alias
:return: model name
:rtype: str
.. method:: get_alias_values()
-
+
Return values to create an alias, or to write on the alias after its
creation. While not completely mandatory, it is usually required to make
sure that newly created records will be linked to the alias' parent (i.e.
tasks getting created in the right project) by setting a dictionary of
default values in the alias' ``alias_defaults`` field.
-
+
:return: dictionnary of values that will be written to the new alias
:rtype: dict
@@ -583,24 +583,24 @@ The ``get_alias_values()`` override is particularly interesting as it allows you
to modify the behaviour of your aliases easily. Among the fields that can be set
on the alias, the following are of particular interest:
-``alias_name`` - :class:`~odoo.fields.Char`
+``alias_name`` - :class:`~odoo.fields.Char`
name of the email alias, e.g. 'jobs' if you want to catch emails for
<jobs@example.odoo.com>
-``alias_user_id`` - :class:`~odoo.fields.Many2one` (``res.users``)
+``alias_user_id`` - :class:`~odoo.fields.Many2one` (``res.users``)
owner of records created upon receiving emails on this alias;
if this field is not set the system will attempt to find the right owner
based on the sender (From) address, or will use the Administrator account
if no system user is found for that address
-``alias_defaults`` - :class:`~odoo.fields.Text`
+``alias_defaults`` - :class:`~odoo.fields.Text`
Python dictionary that will be evaluated to provide
default values when creating new records for this alias
-``alias_force_thread_id`` - :class:`~odoo.fields.Integer`
+``alias_force_thread_id`` - :class:`~odoo.fields.Integer`
optional ID of a thread (record) to which all incoming messages will be
attached, even if they did not reply to it; if set, this will disable the
creation of new records completely
-``alias_contact`` - :class:`~odoo.fields.Selection`
+``alias_contact`` - :class:`~odoo.fields.Selection`
Policy to post a message on the document using the mailgateway
-
+
- *everyone*: everyone can post
- *partners*: only authenticated partners
- *followers*: only followers of the related document or members of following channels
@@ -631,7 +631,7 @@ you to make your alias easily configurable from the record's form view.
expense_ids = fields.One2many('business.expense', 'trip_id', 'Expenses')
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict",
required=True)
-
+
def get_alias_model_name(self, vals):
""" Specify the model that will get created when the alias receives a message """
return 'business.expense'
@@ -641,7 +641,7 @@ you to make your alias easily configurable from the record's form view.
values = super(BusinessTrip, self).get_alias_values()
# alias_defaults holds a dictionnary that will be written
# to all records created by this alias
- #
+ #
# in this case, we want all expense records sent to a trip alias
# to be linked to the corresponding business trip
values['alias_defaults'] = {'trip_id': self.id}
@@ -649,7 +649,7 @@ you to make your alias easily configurable from the record's form view.
# by default
values['alias_contact'] = 'followers'
return values
-
+
class BusinessExpense(models.Model):
_name = 'business.expense'
_inherit = ['mail.thread']
@@ -662,9 +662,9 @@ you to make your alias easily configurable from the record's form view.
We would like our alias to be easily configurable from the form view of our
business trips, so let's add the following to our form view:
-
+
.. code-block:: xml
-
+
<page string="Emails">
<group name="group_alias">
<label for="alias_name" string="Email Alias"/>
@@ -684,7 +684,7 @@ you to make your alias easily configurable from the record's form view.
string="Accept Emails From"/>
</group>
</page>
-
+
Now we can change the alias address directly from the form view and change
who can send e-mails to the alias.
@@ -696,12 +696,12 @@ you to make your alias easily configurable from the record's form view.
class BusinessExpense(models.Model):
# Previous code goes here
# ...
-
+
def message_new(self, msg, custom_values=None):
- """ Override to set values according to the email.
-
+ """ Override to set values according to the email.
+
In this simple example, we simply use the email title as the name
- of the expense, try to find a partner with this email address and
+ of the expense, try to find a partner with this email address and
do a regex match to find the amount of the expense."""
name = msg_dict.get('subject', 'New Expense')
# Match the last occurence of a float in the string
@@ -728,7 +728,7 @@ Activities tracking
-------------------
Activities are actions users have to take on a document like making a phone call
-or organizing a meeting. Activities come with the mail module as they are
+or organizing a meeting. Activities come with the mail module as they are
integrated in the Chatter but are *not bundled with mail.thread*. Activities
are records of the ``mail.activity`` class, which have a type (``mail.activity.type``),
name, description, scheduled time (among others). Pending activities are visible
@@ -798,16 +798,16 @@ campaigns through arguments in links to specified resources. The mixin adds
* ``campaign_id``: :class:`~odoo.fields.Many2one` field to a ``utm.campaign``
object (i.e. Christmas_Special, Fall_Collection, etc.)
-* ``source_id``: :class:`~odoo.fields.Many2one` field to a ``utm.source``
+* ``source_id``: :class:`~odoo.fields.Many2one` field to a ``utm.source``
object (i.e. Search Engine, mailing list, etc.)
-* ``medium_id``: :class:`~odoo.fields.Many2one` field to a ``utm.medium``
+* ``medium_id``: :class:`~odoo.fields.Many2one` field to a ``utm.medium``
object (i.e. Snail Mail, e-Mail, social network update, etc.)
These models have a single field ``name`` (i.e. they are simply there to
distinguish campaigns but don't have any specific behaviour).
Once a customer visits your website with these parameters set in the url
-(i.e. http://www.odoo.com/?campaign_id=mixin_talk&source_id=www.odoo.com&medium_id=website),
+(i.e. https://www.odoo.com/?campaign_id=mixin_talk&source_id=www.odoo.com&medium_id=website),
three cookies are set in the visitor's website for these parameters.
Once a object that inherits the utm.mixin is created from the website (i.e. lead
form, job application, etc.), the utm.mixin code kicks in and fetches the values
@@ -834,7 +834,7 @@ model should support the *quick create* (i.e. call to ``create()`` with a single
_description = 'My Tracked Object'
my_field = fields.Many2one('my_module.my_track', 'My Field')
-
+
@api.model
def tracking_fields(self):
result = super(MyModel, self).tracking_fields()
@@ -884,7 +884,7 @@ This mixin adds 2 fields on your model:
the status of the publication
* ``website_url``: :class:`~odoo.fields.Char` field which represents
the URL through which the object is accessed
-
+
Note that this last field is a computed field and must be implemented for your class:
.. code-block:: python
@@ -918,13 +918,13 @@ buttons to website visitors:
</t>
</div>
-Note that you must pass your object as the variable ``object`` to the template;
+Note that you must pass your object as the variable ``object`` to the template;
in this example, the ``blog.post`` record was passed as the ``blog_post`` variable
to the ``qweb`` rendering engine, it is necessary to specify this to the publish
management template. The ``publish_edit`` variable allow the frontend
button to link to the backend (allowing you to switch from frontend to backend
and vice-versa easily); if set, you must specify the full external id of the action
-you want to call in the backend in the ``action`` variable (note that a Form View
+you want to call in the backend in the ``action`` variable (note that a Form View
must exist for the model).
The action ``website_publish_button`` is defined in the mixin and adapts its
@@ -987,7 +987,7 @@ To add rating support, simply inherit the ``rating.mixin`` model:
class MyModel(models.Models):
_name = 'my_module.my_model'
_inherit = ['rating.mixin', 'mail.thread']
-
+
user_id = fields.Many2one('res.users', 'Responsible')
partner_id = fields.Many2one('res.partner', 'Customer')
@@ -995,18 +995,18 @@ The behaviour of the mixin adapts to your model:
* The ``rating.rating`` record will be linked to the ``partner_id`` field of your
model (if the field is present).
-
+
- this behaviour can be overriden with the function ``rating_get_partner_id()``
if you use another field than ``partner_id``
-
+
* The ``rating.rating`` record will be linked to the partner of the ``user_id``
field of your model (if the field is present) (i.e. the partner who is rated)
-
+
- this behaviour can be overriden with the function ``rating_get_rated_partner_id()``
if you use another field than ``user_id`` (note that the function must return a
``res.partner``, for ``user_id`` the system automatically fetches the partner
of the user)
-
+
* The chatter history will display the rating event (if your model inherits from
``mail.thread``)
@@ -1036,7 +1036,7 @@ links to the rating object. A very basic email template could look like this:
</ul>
]]></field>
</record>
-
+
Your customer will then receive an e-mail with links to a simple webpage allowing
them to provide a feedback on their interaction with your users (including a free-text
feedback message).
@@ -1074,4 +1074,3 @@ You can find concrete examples of integration in the following models:
* ``project.task`` in the Project (*rating_project*) Application
* ``helpdesk.ticket`` in the Helpdesk (*helpdesk* - Odoo Enterprise only) Application
-
diff --git a/doc/reference/module.rst b/doc/reference/module.rst
index 3ab1b8c9b38..437d2c411ee 100644
--- a/doc/reference/module.rst
+++ b/doc/reference/module.rst
@@ -106,7 +106,7 @@ Available manifest fields are:
located in ``static/src/css`` inside the module.
``images`` (``list(str)``)
Specify image files to be used by the module.
-``installable`` (``bool`` default: ``False``)
+``installable`` (``bool`` default: ``True``)
Whether a user should be able to install the module from the Web UI or not.
``maintainer`` (``str``)
Person or entity in charge of the maintenance of this module, by default
@@ -128,6 +128,6 @@ Available manifest fields are:
These hooks should only be used when setup/cleanup required for this module
is either extremely difficult or impossible through the api.
-.. _semantic versioning: http://semver.org
+.. _semantic versioning: https://semver.org
.. _existing categories:
https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst
index 5d910d3b061..a59785d9f71 100644
--- a/doc/reference/qweb.rst
+++ b/doc/reference/qweb.rst
@@ -42,7 +42,7 @@ will result in::
.. _reference/qweb/output:
-data output
+Data output
===========
QWeb has a primary output directive which automatically HTML-escape its
@@ -63,7 +63,7 @@ sanitized user-provided markup.
.. _reference/qweb/conditionals:
-conditionals
+Conditionals
============
QWeb has a conditional directive ``if``, which evaluates an expression given
@@ -107,7 +107,7 @@ available::
.. _reference/qweb/loops:
-loops
+Loops
=====
QWeb has an iteration directive ``foreach`` which take an expression returning
@@ -334,7 +334,7 @@ Python
Exclusive directives
--------------------
-asset bundles
+Asset bundles
'''''''''''''
.. todo:: have fme write these up because I've no idea how they work
@@ -350,7 +350,7 @@ website's rich text edition.
``t-options`` can be used to customize fields, the most common option
is ``widget``, other options are field- or widget-dependent.
-debugging
+Debugging
---------
``t-debug``
@@ -551,6 +551,8 @@ Helpers
:js:func:`core.qweb.render <QWeb2.Engine.render>` can be used to
easily render basic module templates
+.. _reference/qweb/api:
+
API
---
@@ -637,13 +639,13 @@ API
use case. Odoo 9.0 still depends on Jinja_ and Mako_.
.. _templating:
- http://en.wikipedia.org/wiki/Template_processor
+ https://en.wikipedia.org/wiki/Template_processor
.. _Jinja: http://jinja.pocoo.org
-.. _Mako: http://www.makotemplates.org
-.. _Genshi: http://genshi.edgewall.org
-.. _XML namespaces: http://en.wikipedia.org/wiki/XML_namespace
-.. _HTML: http://en.wikipedia.org/wiki/HTML
-.. _XSS: http://en.wikipedia.org/wiki/Cross-site_scripting
+.. _Mako: https://www.makotemplates.org
+.. _Genshi: https://genshi.edgewall.org
+.. _XML namespaces: https://en.wikipedia.org/wiki/XML_namespace
+.. _HTML: https://en.wikipedia.org/wiki/HTML
+.. _XSS: https://en.wikipedia.org/wiki/Cross-site_scripting
.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
-.. _CSS selector: http://api.jquery.com/category/selectors/
+.. _CSS selector: https://api.jquery.com/category/selectors/
diff --git a/doc/reference/reports.rst b/doc/reference/reports.rst
index f12efdcab07..696eabc5809 100644
--- a/doc/reference/reports.rst
+++ b/doc/reference/reports.rst
@@ -129,13 +129,13 @@ you need to define two templates:
* The main report template
* The translatable document
-You can then call the translatable document from your main template with the attribute
+You can then call the translatable document from your main template with the attribute
``t-lang`` set to a language code (for example ``fr`` or ``en_US``) or to a record field.
You will also need to re-browse the related records with the proper context if you use
fields that are translatable (like country names, sales conditions, etc.)
.. warning::
-
+
If your report template does not use translatable record fields, re-browsing the record
in another language is *not* necessary and will impact performances.
@@ -169,10 +169,10 @@ For example, let's look at the Sale Order report from the Sale module::
</template>
-The main template calls the translatable template with ``doc.partner_id.lang`` as a
-``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
-each Sale Order will be printed in the language of the corresponding customer. If you wish
-to translate only the body of the document, but keep the header and footer in a default
+The main template calls the translatable template with ``doc.partner_id.lang`` as a
+``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
+each Sale Order will be printed in the language of the corresponding customer. If you wish
+to translate only the body of the document, but keep the header and footer in a default
language, you could call the report's external layout this way::
<t t-call="web.external_layout" t-lang="en_US">
@@ -180,7 +180,7 @@ language, you could call the report's external layout this way::
.. tip::
Please take note that this works only when calling external templates, you will not be
- able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
+ able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
than ``t-call``. If you wish to translate part of a template, you can create an external
template with this partial template and call it from the main one with the ``t-lang``
attribute.
@@ -345,4 +345,4 @@ For example, you can access a Sale Order report in html mode by going to
Or you can access the pdf version at
\http://<server-address>/report/pdf/sale.report_saleorder/38
-.. _wkhtmltopdf: http://wkhtmltopdf.org
+.. _wkhtmltopdf: https://wkhtmltopdf.org
diff --git a/doc/reference/test-report.txt b/doc/reference/test-report.txt
deleted file mode 100644
index 795bc044074..00000000000
--- a/doc/reference/test-report.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-test_empty_find (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
-test_ids_shortcut (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
-test_regular_find (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
-web.testing.stack: direct, value, success ... ok
-web.testing.stack: direct, deferred, success ... ok
-web.testing.stack: direct, value, error ... ok
-web.testing.stack: direct, deferred, failure ... ok
-web.testing.stack: successful setup ... ok
-web.testing.stack: successful teardown ... ok
-web.testing.stack: successful setup and teardown ... ok
-
-[snip ~150 lines]
-
-test_convert_complex_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_complex_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_literal_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_literal_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_retrieve_nonliteral_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_retrieve_nonliteral_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-
-----------------------------------------------------------------------
-Ran 181 tests in 15.706s
-
-OK
-
diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst
index c1d9410bc4a..12ab6609829 100644
--- a/doc/reference/testing.rst
+++ b/doc/reference/testing.rst
@@ -43,12 +43,6 @@ and ``__init__.py`` contains::
test modules which are not imported from ``tests/__init__.py`` will not be
run
-.. versionchanged:: 8.0
-
- previously, the test runner would only run modules added to two lists
- ``fast_suite`` and ``checks`` in ``tests/__init__.py``. In 8.0 it will
- run all imported modules
-
The test runner will simply run any test case, as described in the official
`unittest documentation`_, but Odoo provides a number of utilities and helpers
related to testing Odoo content (modules, mainly):
@@ -108,10 +102,7 @@ Tests are automatically run when installing or updating modules if
:option:`--test-enable <odoo-bin --test-enable>` was enabled when starting the
Odoo server.
-As of Odoo 8, running tests outside of the install/update cycle is not
-supported.
-
-.. _unittest documentation: https://docs.python.org/2/library/unittest.html
+.. _unittest documentation: https://docs.python.org/3/library/unittest.html
Test selection
--------------
@@ -514,7 +505,7 @@ test runner will then create a phantom_js browser, point it to the proper url
and simulate the click and inputs, according to the scenario.
-.. _qunit: http://qunitjs.com/
+.. _qunit: https://qunitjs.com/
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
.. _web.js_tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L427
.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L509
@@ -523,4 +514,4 @@ and simulate the click and inputs, according to the scenario.
.. _test_utils.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils.js
.. _mock server: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/mock_server.js
.. _qunit assertions: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_asserts.js
-.. _createView: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils_create.js#L267 \ No newline at end of file
+.. _createView: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils_create.js#L267
diff --git a/doc/reference/translations.rst b/doc/reference/translations.rst
index 81ec506df63..b30bb58d3d9 100644
--- a/doc/reference/translations.rst
+++ b/doc/reference/translations.rst
@@ -7,6 +7,11 @@
Translating Modules
===================
+This section explains how to provide translation abilities to your module.
+
+.. note:: If you want to contribute to the translation of Odoo itself, please refer to the
+ `Odoo Wiki page <https://github.com/odoo/odoo/wiki/Translations>`_.
+
Exporting translatable term
===========================
@@ -184,6 +189,6 @@ Read vs Run Time
};
-.. _PO File: http://en.wikipedia.org/wiki/Gettext#Translating
-.. _msginit: http://www.gnu.org/software/gettext/manual/gettext.html#Creating
-.. _POEdit: http://poedit.net/
+.. _PO File: https://en.wikipedia.org/wiki/Gettext#Translating
+.. _msginit: https://www.gnu.org/software/gettext/manual/gettext.html#Creating
+.. _POEdit: https://poedit.net/
diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst
index f1926eca4ea..c626bc133bd 100644
--- a/doc/setup/deploy.rst
+++ b/doc/setup/deploy.rst
@@ -624,16 +624,16 @@ of the supported browsers is the following:
environment than over the internet.
.. _regular expression: https://docs.python.org/3/library/re.html
-.. _ARP spoofing: http://en.wikipedia.org/wiki/ARP_spoofing
+.. _ARP spoofing: https://en.wikipedia.org/wiki/ARP_spoofing
.. _Nginx termination example:
- http://nginx.com/resources/admin-guide/nginx-ssl-termination/
+ https://nginx.com/resources/admin-guide/nginx-ssl-termination/
.. _Nginx proxying example:
- http://nginx.com/resources/admin-guide/reverse-proxy/
+ https://nginx.com/resources/admin-guide/reverse-proxy/
.. _socat: http://www.dest-unreach.org/socat/
.. _PostgreSQL connection settings:
.. _listen to network interfaces:
- http://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
+ https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
.. _use an SSH tunnel:
- http://www.postgresql.org/docs/9.6/static/ssh-tunnels.html
-.. _WSGI: http://wsgi.readthedocs.org/
+ https://www.postgresql.org/docs/9.6/static/ssh-tunnels.html
+.. _WSGI: https://wsgi.readthedocs.org/
.. _POSBox: https://www.odoo.com/page/point-of-sale-hardware#part_2
diff --git a/doc/setup/install.rst b/doc/setup/install.rst
index 7393292eb46..d42becb4f90 100644
--- a/doc/setup/install.rst
+++ b/doc/setup/install.rst
@@ -1,5 +1,6 @@
:banner: banners/installing_odoo.jpg
+
.. _setup/install:
===============
@@ -27,6 +28,7 @@ This documents attempts to describe most of the installation options.
If you usually use docker_ for development or deployment, an official
docker_ base image is available.
+
.. _setup/install/editions:
Editions
@@ -40,6 +42,7 @@ available to anyone.
If you already use the Community version and wish to upgrade to Enterprise, please
refer to :ref:`setup/enterprise` (except for :ref:`setup/install/source`).
+
.. _setup/install/online:
Online
@@ -67,6 +70,7 @@ Can be used for both testing Odoo and long-term production use.
Like demo_ instances, SaaS_ instances require no local installation, a web
browser is sufficient.
+
.. _setup/install/packaged:
Packaged installers
@@ -81,14 +85,14 @@ but may be difficult to keep up-to-date.
Official Community packages with all relevant dependency requirements are
available on our nightly_ server. Both Communtiy and Enterprise packages can
-be downloaded from our Download_ page (you must to be logged in as a paying
+be downloaded from our download_ page (you must to be logged in as a paying
customer or partner to download the Enterprise packages).
Windows
-------
* Download the installer from our nightly_ server (Community only)
- or the Windows installer from the Download_ page (any edition)
+ or the Windows installer from the download_ page (any edition)
* Run the downloaded file
.. warning:: on Windows 8, you may see a warning titled "Windows protected
@@ -252,325 +256,475 @@ Once downloaded, the package can be installed using the 'dnf' package manager:
$ sudo systemctl enable odoo
$ sudo systemctl start odoo
+
.. _setup/install/source:
Source Install
==============
-The source "installation" really is about not installing Odoo, and running
-it directly from source instead.
+The source "installation" is really about not installing Odoo, and running it directly from source
+instead.
-This can be more convenient for module developers as the Odoo source is
-more easily accessible than using packaged installation (for information or
-to build this documentation and have it available offline).
+This can be more convenient for module developers as the Odoo source is more easily accessible
+than using packaged installation (for information or to build this documentation and have it
+available offline).
-It also makes starting and stopping Odoo more flexible and explicit than the
-services set up by the packaged installations, and allows overriding settings
-using :ref:`command-line parameters <reference/cmdline>` without needing to
-edit a configuration file.
+It also makes starting and stopping Odoo more flexible and explicit than the services set up by the
+packaged installations, and allows overriding settings using
+:ref:`command-line parameters <reference/cmdline>` without needing to edit a configuration file.
-Finally it provides greater control over the system's set up, and allows more
-easily keeping (and running) multiple versions of Odoo side-by-side.
+Finally it provides greater control over the system's set up, and allows to more easily keep
+(and run) multiple versions of Odoo side-by-side.
-Prepare
+Windows
-------
-Source installation requires manually installing dependencies:
+Fetch the sources
+'''''''''''''''''
+
+There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
+
+Archive
+^^^^^^^
+
+Community Edition:
-* Python 3.5+.
+* `Official download page <download_>`_
+* `GitHub repository <community-repository_>`_
+* `Nightly server <nightly_>`_
- - on Linux and OS X, using your package manager if not installed by default
+Enterprise Edition:
- .. note:: on some system, ``python`` command refers to Python 2 (outdated)
- or to Python 3 (supported). Make sure you are using the right
- version and that the alias ``python3`` is present in your
- :envvar:`PATH`
+* `Official download page <download_>`_
+* `GitHub repository <enterprise-repository_>`_
- - on Windows, use `the official Python 3 installer
- <https://www.python.org/downloads/windows/>`_.
+Git
+^^^
- .. warning:: select "add python.exe to Path" during installation, and
- reboot afterwards to ensure the :envvar:`PATH` is updated
+The following requires git_ to be installed on your machine and that you have basic knowledge of
+git commands.
- .. note:: if Python is already installed, make sure it is 3.5 or above,
- previous versions are not compatible with Odoo.
+Community Edition:
-* PostgreSQL, to use a local database
+.. code-block:: doscon
- After installation you will need to create a postgres user: by default the
- only user is ``postgres``, and Odoo forbids connecting as ``postgres``.
+ C:\> git clone https://github.com/odoo/odoo.git
- - on Linux, use your distribution's package, then create a postgres user
- named like your login:
- .. code-block:: console
+Enterprise Edition: (see :ref:`setup/install/editions` to get access)
- $ sudo su - postgres -c "createuser -s $USER"
+.. code-block:: doscon
- Because the role login is the same as your unix login unix sockets can be
- use without a password.
+ C:\> git clone https://github.com/odoo/enterprise.git
- - on OS X, `postgres.app <http://postgresapp.com>`_ is the simplest way to
- get started, then create a postgres user as on Linux
+.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
+ a collection of extra add-ons. The main server code is in the Community version. Running
+ the Enterprise version actually means running the server from the Community version with
+ the addons-path option set to the folder with the Enterprise version. You need to clone
+ both the Community and Enterprise repository to have a working Odoo Enterprise
+ installation.
- - on Windows, use `PostgreSQL for windows`_ then
+Prepare
+'''''''
- - add PostgreSQL's ``bin`` directory (default:
- ``C:\Program Files\PostgreSQL\9.4\bin``) to your :envvar:`PATH`
- - create a postgres user with a password using the pg admin gui: open
- pgAdminIII, double-click the server to create a connection, select
- :menuselection:`Edit --> New Object --> New Login Role`, enter the
- usename in the :guilabel:`Role Name` field (e.g. ``odoo``), then open
- the :guilabel:`Definition` tab and enter the password (e.g. ``odoo``),
- then click :guilabel:`OK`.
+Python
+^^^^^^
- The user and password must be passed to Odoo using either the
- :option:`-w <odoo-bin -w>` and :option:`-r <odoo-bin -r>` options or
- :ref:`the configuration file <reference/cmdline/config>`
+Odoo requires Python 3.5 or later to run. Use the the official `Python 3 installer
+<https://www.python.org/downloads/windows/>`_ to download and install Python 3 on your machine.
-* Python dependencies listed in the :file:`requirements.txt` file.
+During installation, check **Add Python 3 to PATH**, then click **Customize Installation** and make
+sure that **pip** is checked.
- - on Linux, python dependencies may be installable with the system's package
- manager or using pip.
+.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
+ are not compatible with Odoo.
- For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2,
- ldap) it may be necessary to install development tools and native
- dependencies before pip is able to install the dependencies themselves.
- These are available in ``-dev`` or ``-devel`` packages for Python,
- Postgres, libxml2, libxslt, libevent, libsasl2 and libldap2. Then the Python
- dependecies can themselves be installed:
+ .. code-block:: doscon
- .. code-block:: console
+ C:\> python3 --version
- $ pip3 install -r requirements.txt
+ Verify also that pip_ is installed for this version.
- - on OS X, you will need to install the Command Line Tools
- (``xcode-select --install``) then download and install a package manager
- of your choice (homebrew_, macports_) to install non-Python dependencies.
- pip can then be used to install the Python dependencies as on Linux:
+ .. code-block:: doscon
- .. code-block:: console
+ C:\> pip3 --version
- $ pip3 install -r requirements.txt
+PostgreSQL
+^^^^^^^^^^
- - on Windows you need to install some of the dependencies manually, tweak the
- requirements.txt file, then run pip to install the remaning ones.
+Odoo uses PostgreSQL as database management system. Download and install the `latest version of
+PostgreSQL <https://www.postgresql.org/download/windows/>`_.
- Install ``psycopg`` using the installer here
- http://www.stickpeople.com/projects/python/win-psycopg/
+By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
+create a new PostgreSQL user:
- Then use pip to install the dependencies using the following
- command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual
- path where you downloaded Odoo):
+#. Add PostgreSQL's `bin` directory (by default: `C:\\Program Files\\PostgreSQL\\<version>\\bin`) to
+ your `PATH`.
+#. Create a postgres user with a password using the pg admin gui:
- .. code-block:: doscon
+ * Open **pgAdminIII**.
+ * Double-click the server to create a connection.
+ * Select :menuselection:`Edit --> New Object --> New Login Role`.
+ * Enter the username in the **Role Name** field (e.g. `odoo`).
+ * Open the **Definition** tab and enter the password (e.g. ``odoo``), then click **OK**.
- C:\> cd \YourOdooPath
- C:\YourOdooPath> C:\Python35\Scripts\pip.exe install -r requirements.txt
+Dependencies
+^^^^^^^^^^^^
-* *RTLCSS* via nodejs
+Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
+community directory. Most of them can be installed with **pip**.
- For languages with right-to-left interface (such as Arabic or Hebrew), the
- package ``rtlcss`` is needed.
+.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
+ or with your system. You can use virtualenv_ to create isolated Python environments.
- - on Linux, use your distribution's package manager to install nodejs and
- npm.
- Once npm is installed, use it to install rtlcss:
+Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
+on the requirements file:
- .. code-block:: console
+.. code-block:: doscon
- $ sudo npm install -g rtlcss
+ C:\> cd \YourOdooCommunityPath
+ C:\YourOdooCommunityPath> C:\Python35\Scripts\pip.exe install -r requirements.txt
- - on OS X, install nodejs via your preferred package manager (homebrew_,
- macports_) then install less:
+.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
+ In particular:
- .. code-block:: console
+ * `psycopg` must be installed with
+ `this installer <http://www.stickpeople.com/projects/python/win-psycopg/>`_.
+ * `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
+ for it to support headers and footers. See our
+ `wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
+ various versions.
- $ sudo npm install -g rtlcss
+For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
+needed:
- - on Windows, `install nodejs <https://nodejs.org/en/download/>`_, reboot (to
- update the :envvar:`PATH`) and install rtlcss:
+#. Download and install `nodejs <https://nodejs.org/en/download/>`_.
+#. Install `rtlcss`:
- .. code-block:: doscon
+ .. code-block:: doscon
- C:\> npm install -g rtlcss
+ C:\> npm install -g rtlcss
- It is then necessary to edit the System Environment's variable
- :envvar:`PATH` and add the folder where `rtlcss.cmd` is located. Typically:
+#. Edit the System Environment's variable `PATH` to add the folder where `rtlcss.cmd` is located
+ (typically: `C:\\Users\\<user>\\AppData\\Roaming\\npm\\`).
- .. code-block:: console
+Running Odoo
+''''''''''''
- C:\Users\<user>\AppData\Roaming\npm\
+Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the
+command-line interface of the server. It is located at the root of the Odoo Community directory.
+To configure the server, you can either specify :ref:`command-line arguments <reference/cmdline/server>` or a
+:ref:`configuration file <reference/cmdline/config>`.
-Fetch the sources
------------------
+.. tip:: For the Enterprise edition, you must add the path to the `enterprise` addons to the
+ `addons-path` argument. Note that it must come before the other paths in `addons-path` for
+ addons to be loaded correctly.
+
+Common necessary configurations are:
-There are two ways to obtain the Odoo source code: zip or git.
+* PostgreSQL user and password.
+* Custom addon paths beyond the defaults, to load your own modules.
+
+A typical way to run the server would be:
+
+.. code-block:: doscon
-* Odoo zip can be downloaded from our nightly_ server or our Download_ page,
- the zip file then needs to be uncompressed to use its content
+ C:\YourOdooCommunityPath> python3 odoo-bin -r dbuser -w dbpassword --addons-path=addons,../mymodules --db-filter=mydb$
-* git allows simpler update and easier switching between different versions
- of Odoo. It also simplifies maintaining non-module patches and
- contributions. The primary drawback of git is that it is significantly
- larger than a tarball as it contains the entire history of the Odoo project.
+Where `YourOdooCommunityPath` is the path of the Odoo Community installation, `dbuser` is the
+PostgreSQL login, `dbpassword` is the PostgreSQL password, `../mymodules` is a directory with
+additional addons and `mydb` is the default database to serve on `localhost:8069`.
-Community Edition
+Linux
+-----
+
+Fetch the sources
'''''''''''''''''
-The git repository is https://github.com/odoo/odoo.git for the Community
-edition.
+There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
+
+Archive
+^^^^^^^
+
+Community Edition:
-Downloading it requires a `git client <http://git-scm.com/download/>`_
-(which may be available via your distribution on linux) and can be performed
-using the following command:
+* `Official download page <download_>`_
+* `GitHub repository <community-repository_>`_
+* `Nightly server <nightly_>`_
+
+Enterprise Edition:
+
+* `Official download page <download_>`_
+* `GitHub repository <enterprise-repository_>`_
+
+Git
+^^^
+
+The following requires git_ to be installed on your machine and that you have basic knowledge of
+git commands.
+
+Community Edition:
.. code-block:: console
$ git clone https://github.com/odoo/odoo.git
-Enterprise Edition
-''''''''''''''''''
-If you have access to the Enterprise repository (see :ref:`setup/install/editions`
-if you wish to get access), you can use this command to fetch the addons:
+Enterprise Edition: (see :ref:`setup/install/editions` to get access)
.. code-block:: console
$ git clone https://github.com/odoo/enterprise.git
-.. note:: The Enterprise git repository **does not contain the full Odoo
- source code**. It is only a collection of extra add-ons. The main server
- code is in the Community version. Running the Enterprise version actually
- means running the server from the Community version with the addons-path option
- set to the folder with the Enterprise version.
+.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
+ a collection of extra add-ons. The main server code is in the Community version. Running
+ the Enterprise version actually means running the server from the Community version with
+ the addons-path option set to the folder with the Enterprise version. You need to clone
+ both the Community and Enterprise repository to have a working Odoo Enterprise
+ installation.
- You need to clone both the Community and Enterprise repository to have a working
- Odoo installation
+Prepare
+'''''''
-Running Odoo
-------------
+Python
+^^^^^^
-Once all dependencies are set up, Odoo can be launched by running ``odoo-bin``.
+Odoo requires Python 3.5 or later to run. Use your package manager to download and install Python 3
+on your machine if it is not already done.
-.. tip:: For the Enterprise edition, you must specify the :file:`enterprise`
- addons folder when starting your server. You can do so by providing the path
- to your :file:`enterprise` folder in the ``addons-path`` parameter. Please
- note that the :file:`enterprise` folder must come before the default
- :file:`addons` folder in the list for the addons to be loaded correctly.
+.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
+ are not compatible with Odoo.
-:ref:`Configuration <reference/cmdline>` can be provided either through
-:ref:`command-line arguments <reference/cmdline>` or through a
-:ref:`configuration file <reference/cmdline/config>`.
+ .. code-block:: console
-Common necessary configurations are:
+ $ python3 --version
-* PostgreSQL host, port, user and password.
+ Verify also that pip_ is installed for this version.
- Odoo has no defaults beyond
- `psycopg2's defaults <http://initd.org/psycopg/docs/module.html>`_: connects
- over a UNIX socket on port 5432 with the current user and no password. By
- default this should work on Linux and OS X, but it *will not work* on
- windows as it does not support UNIX sockets.
+ .. code-block:: console
-* Custom addons path beyond the defaults, to load your own modules
+ $ pip3 --version
-Under Windows a typical way to execute odoo would be:
+PostgreSQL
+^^^^^^^^^^
-.. code-block:: doscon
+Odoo uses PostgreSQL as database management system. Use your package manager to download and install
+the latest version of PostgreSQL.
+
+By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
+create a new PostgreSQL user:
- C:\YourOdooPath> python3 odoo-bin -w odoo -r odoo --addons-path=addons,../mymodules --db-filter=mydb$
+.. code-block:: console
+
+ $ sudo -u postgres createuser -s $USER
+ $ createdb $USER
+
+.. note:: Because your PostgreSQL user has the same name as your Unix login, you will be able to
+ connect to the database without password.
-Where ``odoo``, ``odoo`` are the postgresql login and password,
-``../mymodules`` a directory with additional addons and ``mydb`` the default
-db to serve on localhost:8069
+Dependencies
+^^^^^^^^^^^^
-Under Unix a typical way to execute odoo would be:
+Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
+community directory. Most of them can be installed with **pip**.
+
+.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
+ or with your system. You can use virtualenv_ to create isolated Python environments.
+
+Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
+on the requirements file:
.. code-block:: console
- $ ./odoo-bin --addons-path=addons,../mymodules --db-filter=mydb$
+ $ cd /YourOdooCommunityPath
+ /YourOdooCommunityPath$ pip3 install -r requirements.txt
-Where ``../mymodules`` is a directory with additional addons and ``mydb`` the
-default db to serve on localhost:8069
+.. warning:: For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2, ldap), it
+ may be necessary to install development tools and native dependencies before pip is
+ able to install the dependencies themselves. These are available in `-dev` or `-devel`
+ packages for Python, PostgreSQL, libxml2, libxslt, libevent, libsasl2 and libldap2.
-Virtualenv
-----------
+.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
+ In particular:
-Virtualenv_ is a tool to create Python isolated environments because it's
-sometimes preferable to not mix your distribution python modules packages
-with globally installed python modules with pip.
+ * `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
+ for it to support headers and footers. See our
+ `wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
+ various versions.
-This section will explain how to run Odoo in a such isolated Python environment.
+For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
+needed:
-Here we are going to use virtualenvwrapper_ which is a set of shell scripts that
-makes the use of virtualenv easier.
+#. Download and install **nodejs** and **npm** with your package manager.
+#. Install `rtlcss`:
-The examples below are based on a Debian 9 distribution but could be adapted on
-any platform where virtualenvwrapper_ and virtualenv_ are able to run.
+ .. code-block:: console
-This section assumes that you obtained the Odoo sources from the zip file or the
-git repository as explained above. The same apply for postgresql installation
-and configuration.
+ $ sudo npm install -g rtlcss
-Install virtualenvwrapper
-'''''''''''''''''''''''''
+Running Odoo
+''''''''''''
-.. code-block:: console
+Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the
+command-line interface of the server. It is located at the root of the Odoo Community directory.
+
+To configure the server, you can either specify :ref:`command-line arguments <reference/cmdline/server>` or a
+:ref:`configuration file <reference/cmdline/config>`.
- $ sudo apt install virtualenvwrapper
- $ source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
+.. tip:: For the Enterprise edition, you must add the path to the `enterprise` addons to the
+ `addons-path` argument. Note that it must come before the other paths in `addons-path` for
+ addons to be loaded correctly.
-This will install virtualenvwrapper_ and activate it immediately.
-Now, let's install the tools required to build Odoo dependencies if needed:
+Common necessary configurations are:
+
+* PostgreSQL user and password. Odoo has no defaults beyond
+ `psycopg2's defaults <http://initd.org/psycopg/docs/module.html>`_: connects over a UNIX socket on
+ port `5432` with the current user and no password.
+* Custom addon paths beyond the defaults, to load your own modules.
+
+A typical way to run the server would be:
.. code-block:: console
- $ sudo apt install build-essential python3-dev libxslt-dev libzip-dev libldap2-dev libsasl2-dev
+ /YourOdooCommunityPath$ python3 odoo-bin --addons-path=addons,../mymodules --db-filter=mydb$
-Create an isolated environment
-''''''''''''''''''''''''''''''
+Where `YourOdooCommunityPath` is the path of the Odoo Community installation, `../mymodules` is a
+directory with additional addons and `mydb` is the default database to serve on `localhost:8069`.
-Now we can create a virtual environment for Odoo like this:
+Mac OS
+------
-.. code-block:: console
+Fetch the sources
+'''''''''''''''''
+
+There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
+
+Archive
+^^^^^^^
+
+Community Edition:
- $ mkvirtualenv -p /usr/bin/python3 odoo-venv
+* `Official download page <download_>`_
+* `GitHub repository <community-repository_>`_
+* `Nightly server <nightly_>`_
-With this command, we ask for an isolated Python3 environment that will be named
-"odoo-env". If the command works as expected, your shell is now using this
-environment. Your prompt should have changed to remind you that you are using
-an isolated environment. You can verify with this command:
+Enterprise Edition:
+
+* `Official download page <download_>`_
+* `GitHub repository <enterprise-repository_>`_
+
+Git
+^^^
+
+The following requires git_ to be installed on your machine and that you have basic knowledge of
+git commands.
+
+Community Edition:
.. code-block:: console
- $ which python3
+ $ git clone https://github.com/odoo/odoo.git
-This command should show you the path to the Python interpreter located in the
-isolated environment directory.
-Now let's install the Odoo required python packages:
+Enterprise Edition: (see :ref:`setup/install/editions` to get access)
.. code-block:: console
- $ cd your_odoo_sources_path
- $ pip install -r requirements.txt
+ $ git clone https://github.com/odoo/enterprise.git
+
+.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
+ a collection of extra add-ons. The main server code is in the Community version. Running
+ the Enterprise version actually means running the server from the Community version with
+ the addons-path option set to the folder with the Enterprise version. You need to clone
+ both the Community and Enterprise repository to have a working Odoo Enterprise
+ installation.
+
+Prepare
+'''''''
+
+Python
+^^^^^^
+
+Odoo requires Python 3.5 or later to run. Use your preferred package manager (homebrew_, macports_)
+to download and install Python 3 on your machine if it is not already done.
+
+.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
+ are not compatible with Odoo.
-After a little while, you should be ready to run odoo from the command line as
-explained above.
+ .. code-block:: console
-When you you want to leave the virtual environment, just issue this command:
+ $ python3 --version
+
+ Verify also that pip_ is installed for this version.
+
+ .. code-block:: console
+
+ $ pip3 --version
+
+PostgreSQL
+^^^^^^^^^^
+
+Odoo uses PostgreSQL as database management system. Use `postgres.app <https://postgresapp.com>`_
+to download and install the latest version of PostgreSQL.
+
+By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
+create a new PostgreSQL user:
.. code-block:: console
- $ deactivate
+ $ sudo -u postgres createuser -s $USER
+ $ createdb $USER
-Whenever you want to work again with your 'odoo-venv' environment:
+.. note:: Because your PostgreSQL user has the same name as your Unix login, you will be able to
+ connect to the database without password.
+
+Dependencies
+^^^^^^^^^^^^
+
+Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
+community directory. Most of them can be installed with **pip**.
+
+.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
+ or with your system. You can use virtualenv_ to create isolated Python environments.
+
+Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
+on the requirements file:
.. code-block:: console
- $ workon odoo-venv
+ $ cd /YourOdooCommunityPath
+ /YourOdooCommunityPath$ pip3 install -r requirements.txt
+
+.. warning:: Non-Python dependencies need to be installed with a package manager:
+
+ #. Download and install the **Command Line Tools**:
+
+ .. code-block:: console
+
+ $ xcode-select --install
+
+ #. Download and install the package manager of your choice (homebrew_, macports_).
+ #. Install non-python dependencies.
+
+.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
+ In particular:
+
+ * `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
+ for it to support headers and footers. See our
+ `wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
+ various versions.
+
+For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
+needed:
+
+#. Download and install **nodejs** with your preferred package manager (homebrew_, macports_).
+#. Install `rtlcss`:
+
+ .. code-block:: console
+
+ $ sudo npm install -g rtlcss
+
.. _setup/install/docker:
@@ -580,18 +734,16 @@ Docker
The full documentation on how to use Odoo with Docker can be found on the
official Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
+.. _Debian Stretch: https://www.debian.org/releases/stretch/
.. _demo: https://demo.odoo.com
.. _docker: https://www.docker.com
-.. _Download: https://www.odoo.com/page/download
-.. _Debian Stretch: https://www.debian.org/releases/stretch/
+.. _download: https://www.odoo.com/page/download
.. _Ubuntu 18.04: http://releases.ubuntu.com/18.04/
.. _EPEL: https://fedoraproject.org/wiki/EPEL
.. _PostgreSQL: http://www.postgresql.org
.. _the official installer:
.. _install pip:
https://pip.pypa.io/en/latest/installing.html#install-pip
-.. _PostgreSQL for windows:
- http://www.enterprisedb.com/products-services-training/pgdownload
.. _Quilt: http://en.wikipedia.org/wiki/Quilt_(software)
.. _saas: https://www.odoo.com/page/start
.. _the wkhtmltopdf download page: https://github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.5
@@ -604,8 +756,9 @@ official Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
.. _virtualenv: https://pypi.python.org/pypi/virtualenv
.. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io/en/latest/
.. _pywin32: http://sourceforge.net/projects/pywin32/files/pywin32/
-.. _the repository: https://github.com/odoo/odoo
-.. _git: http://git-scm.com
+.. _community-repository: https://github.com/odoo/odoo
+.. _enterprise-repository: https://github.com/odoo/enterprise
+.. _git: https://git-scm.com/
.. _Editions: https://www.odoo.com/pricing#pricing_table_features
-.. _nightly: https://nightly.odoo.com/12.0/nightly/
+.. _nightly: https://nightly.odoo.com/
.. _extra: https://nightly.odoo.com/extra/
diff --git a/doc/webservices/iap.rst b/doc/webservices/iap.rst
index 2dec1fb1dc0..f796809b190 100644
--- a/doc/webservices/iap.rst
+++ b/doc/webservices/iap.rst
@@ -75,7 +75,7 @@ Overview
service directly, or you can delegate the actual service acting as a
bridge/translator between an Odoo system and the actual service.
-
+
.. figure:: images/credits.jpg
:align: center
@@ -94,7 +94,7 @@ Overview
* for a postage service: 1 credit = 1 post stamp.
A credit can also simply be associated with a fixed amount of money
- to palliate the variations of price (e.g. the prices of sms and stamps
+ to palliate the variations of price (e.g. the prices of sms and stamps
may vary following the countries).
The value of the credits is fixed with the help of prepaid credit packs
@@ -158,15 +158,15 @@ Register the service on Odoo
.. todo:: complete this part with screenshots
-The first step is to register your service on the IAP endpoint (production
+The first step is to register your service on the IAP endpoint (production
and/or test) before you can actually query user accounts. To create a service,
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
production, https://iap-sandbox.odoo.com for testing, the endpoints are
*independent* and *not synchronized*). Alternatively, you can go to your portal
on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*.
-.. note::
-
+.. note::
+
On production, there is a manual validation step before the service
can be used to manage real transactions. This step is automatically passed when
on sandbox to ease the tests.
@@ -187,7 +187,7 @@ The service has *seven* important fields:
.. warning::
- Both the :class:`ServiceName` and :class:`Label` are unique. As good practice, the
+ Both the :class:`ServiceName` and :class:`Label` are unique. As good practice, the
:class:`ServiceName` should usually match the name of your Odoo Client App.
* :samp:`icon` - :class:`Icon`: A generic icon that will serve as default for your
@@ -241,8 +241,8 @@ A credit pack is essentially a product with five characteristics:
* Amount: amount of credits the client is entitled to when buying the pack,
* Price: price in EUR (for the time being, USD support is planned).
-.. note::
-
+.. note::
+
Odoo takes a 25% commission on all pack sales. Adjust your selling price accordingly.
@@ -300,7 +300,7 @@ In that call, we will need to provide:
* the :class:`token <UserToken>` of the current client that is provided by
the ``iap.account`` model's ``account_token`` field. You can retrieve the
account for your service by calling :samp:`env['iap.account'].get({service_name})`
- where :class:`service_name <ServiceName>` is the name of the service registered
+ where :class:`service_name <ServiceName>` is the name of the service registered
on IAP endpoint.
.. patch::
@@ -353,7 +353,7 @@ The :class:`~odoo.addons.iap.models.iap.charge` helper will:
if the account does not have enough credits it will raise the relevant
error
2. execute the body of the ``with`` statement
-3. if the body of the ``with`` executes succesfully, update the price
+3. if the body of the ``with`` executes succesfully, update the price
of the transaction if needed
4. capture (confirm) the transaction
5. otherwise, if an error is raised from the body of the ``with``, cancel the
@@ -465,7 +465,7 @@ Capture
.. code-block:: python
:emphasize-lines: 8
-
+
r2 = requests.post(ODOO + '/iap/1/capture', json={
'jsonrpc': '2.0',
'id': None,
@@ -550,6 +550,7 @@ care how they are implemented.
pending transactions/existing holds).
.. exception:: odoo.exceptions.AccessError
+ :noindex:
Raised by:
@@ -557,8 +558,9 @@ care how they are implemented.
* any failure in an inter-server call. (typically, in :func:`~odoo.addons.iap.jsonrpc`).
.. exception:: odoo.exceptions.UserError
+ :noindex:
- Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
+ Raised by any unexpected behaviour at the discretion of the App developer (*you*).
Test the API
@@ -687,7 +689,7 @@ Cancel
------
.. class:: odoo.addons.iap.models.iap.cancel(env, transaction_token, key)
-
+
Will cancel an authorized transaction.
:param odoo.api.Environment env: used to retrieve the ``iap.endpoint``
@@ -761,5 +763,5 @@ Capture
capture(env,transaction_token, key, min(credits, 2))
-.. _JSON-RPC2: http://www.jsonrpc.org/specification
+.. _JSON-RPC2: https://www.jsonrpc.org/specification
.. _Odoo App: https://www.odoo.com/apps
diff --git a/doc/webservices/odoo.rst b/doc/webservices/odoo.rst
index 459da784ecb..288659c95da 100644
--- a/doc/webservices/odoo.rst
+++ b/doc/webservices/odoo.rst
@@ -184,13 +184,13 @@ database:
These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
library, which provides a simple XML-RPC API. Ripcord requires that
`XML-RPC support be enabled
- <http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
+ <https://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
installation.
Since calls are performed over
- `HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
+ `HTTPS <https://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
the `OpenSSL extension
- <http://php.net/manual/en/openssl.installation.php>`_ be enabled.
+ <https://php.net/manual/en/openssl.installation.php>`_ be enabled.
.. case:: Java
@@ -219,7 +219,7 @@ database:
Logging in
----------
-Odoo requires users of the API to be authenticated before they can query most
+Odoo requires users of the API to be authenticated before they can query most
data.
The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
@@ -414,31 +414,31 @@ available to only retrieve a subset of all matched records.
.. container:: doc-aside
.. switcher::
-
+
.. code-block:: python3
-
+
models.execute_kw(db, uid, password,
'res.partner', 'search',
[[['is_company', '=', True], ['customer', '=', True]]],
{'offset': 10, 'limit': 5})
-
+
.. code-block:: ruby
-
+
models.execute_kw(db, uid, password,
'res.partner', 'search',
[[['is_company', '=', true], ['customer', '=', true]]],
{offset: 10, limit: 5})
-
+
.. code-block:: php
-
+
$models->execute_kw($db, $uid, $password,
'res.partner', 'search',
array(array(array('is_company', '=', true),
array('customer', '=', true))),
array('offset'=>10, 'limit'=>5));
-
+
.. code-block:: java
-
+
asList((Object[])models.execute("execute_kw", asList(
db, uid, password,
"res.partner", "search",
@@ -447,9 +447,9 @@ available to only retrieve a subset of all matched records.
asList("customer", "=", true))),
new HashMap() {{ put("offset", 10); put("limit", 5); }}
)));
-
+
.. code-block:: json
-
+
[13, 20, 30, 22, 29]
Count records
@@ -464,28 +464,28 @@ only the number of records matching the query. It takes the same
.. container:: doc-aside
.. switcher::
-
+
.. code-block:: python3
-
+
models.execute_kw(db, uid, password,
'res.partner', 'search_count',
[[['is_company', '=', True], ['customer', '=', True]]])
-
+
.. code-block:: ruby
-
+
models.execute_kw(db, uid, password,
'res.partner', 'search_count',
[[['is_company', '=', true], ['customer', '=', true]]])
-
+
.. code-block:: php
-
+
$models->execute_kw($db, $uid, $password,
'res.partner', 'search_count',
array(array(array('is_company', '=', true),
array('customer', '=', true))));
-
+
.. code-block:: java
-
+
(Integer)models.execute("execute_kw", asList(
db, uid, password,
"res.partner", "search_count",
@@ -493,9 +493,9 @@ only the number of records matching the query. It takes the same
asList("is_company", "=", true),
asList("customer", "=", true)))
));
-
+
.. code-block:: json
-
+
19
.. warning::
@@ -514,11 +514,11 @@ fetch. By default, it will fetch all the fields the current user can read,
which tends to be a huge amount.
.. container:: doc-aside
-
+
.. switcher::
-
+
.. code-block:: python3
-
+
ids = models.execute_kw(db, uid, password,
'res.partner', 'search',
[[['is_company', '=', True], ['customer', '=', True]]],
@@ -527,9 +527,9 @@ which tends to be a huge amount.
'res.partner', 'read', [ids])
# count the number of fields fetched by default
len(record)
-
+
.. code-block:: ruby
-
+
ids = models.execute_kw(db, uid, password,
'res.partner', 'search',
[[['is_company', '=', true], ['customer', '=', true]]],
@@ -538,9 +538,9 @@ which tends to be a huge amount.
'res.partner', 'read', [ids]).first
# count the number of fields fetched by default
record.length
-
+
.. code-block:: php
-
+
$ids = $models->execute_kw($db, $uid, $password,
'res.partner', 'search',
array(array(array('is_company', '=', true),
@@ -550,9 +550,9 @@ which tends to be a huge amount.
'res.partner', 'read', array($ids));
// count the number of fields fetched by default
count($records[0]);
-
+
.. code-block:: java
-
+
final List ids = asList((Object[])models.execute(
"execute_kw", asList(
db, uid, password,
@@ -570,9 +570,9 @@ which tends to be a huge amount.
))[0];
// count the number of fields fetched by default
record.size();
-
+
.. code-block:: json
-
+
121
Conversedly, picking only three fields deemed interesting.
@@ -910,7 +910,7 @@ a record).
Delete records
--------------
-Records can be deleted in bulk by providing their ids to
+Records can be deleted in bulk by providing their ids to
:meth:`~odoo.models.Model.unlink`.
.. container:: doc-aside
@@ -1296,6 +1296,6 @@ activated as actual fields on the model.
]
-.. _PostgreSQL: http://www.postgresql.org
-.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
-.. _base64: http://en.wikipedia.org/wiki/Base64
+.. _PostgreSQL: https://www.postgresql.org
+.. _XML-RPC: https://en.wikipedia.org/wiki/XML-RPC
+.. _base64: https://en.wikipedia.org/wiki/Base64
diff --git a/doc/webservices/upgrade.rst b/doc/webservices/upgrade.rst
index 25310f0e70e..0e166b74be0 100644
--- a/doc/webservices/upgrade.rst
+++ b/doc/webservices/upgrade.rst
@@ -135,7 +135,7 @@ Sample script
Here are 2 examples of database upgrade request creation using:
* one in the python programming language using the requests library
-* one in the bash programming language using `curl <http://curl.haxx.se>`_ (tool
+* one in the bash programming language using `curl <https://curl.haxx.se>`_ (tool
for transfering data using http) and `jq <https://stedolan.github.io/jq>`_ (JSON processor):
.. rst-class:: setup doc-aside
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index 39e77487d3a..db5cb7a3467 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -869,7 +869,7 @@
<record id="KWD" model="res.currency">
<field name="name">KWD</field>
<field name="symbol"> د.ك </field>
- <field name="rounding">0.01</field>
+ <field name="rounding">0.001</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Dinar</field>
<field name="currency_subunit_label">Fils</field>
diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py
index 4afc9a76a56..dc521396b0b 100644
--- a/odoo/addons/base/models/assetsbundle.py
+++ b/odoo/addons/base/models/assetsbundle.py
@@ -488,13 +488,15 @@ class AssetsBundle(object):
compiled = compiled.strip()
# Post process the produced css to add required vendor prefixes here
- compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled);
-
- compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled) # For PhantomJS tests and wkhtmltopdf
- compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled) # For wkhtmltopdf
- compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled) # For PhantomJS tests
- compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled) # For wkhtmltopdf
- compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled) # For PhantomJS tests and wkhtmltopdf
+ compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled)
+
+ # Most of those are only useful for wkhtmltopdf (some for old PhantomJS)
+ compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled)
+ compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled)
+ compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled)
+ compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled)
+ compiled = re.sub(r'(flex-wrap: (\w+);)', r'-webkit-flex-wrap: \2; \1', compiled)
+ compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled)
return compiled
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index c2d7858cc15..6b3d318b4d3 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -331,9 +331,7 @@ class IrActionsServer(models.Model):
The available actions are :
- 'Execute Python Code': a block of python code that will be executed
- - 'Run a Client Action': choose a client action to launch
- - 'Create or Copy a new Record': create a new record with new values, or
- copy an existing record in your database
+ - 'Create a new Record': create a new record with new values
- 'Write on a Record': update the values of a record
- 'Execute several actions': define an action that triggers several other
server actions
@@ -374,11 +372,12 @@ class IrActionsServer(models.Model):
default='object_write', required=True,
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\n"
- "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
- "- 'Write on a Record': update the values of a record\n"
+ "- 'Create': create a new record with new values\n"
+ "- 'Update a Record': update the values of a record\n"
"- 'Execute several actions': define an action that triggers several other server actions\n"
- "- 'Add Followers': add followers to a record (available in Discuss)\n"
- "- 'Send Email': automatically send an email (available in email_template)")
+ "- 'Send Email': automatically send an email (Discuss)\n"
+ "- 'Add Followers': add followers to a record (Discuss)\n"
+ "- 'Create Next Activity': create an activity (Discuss)")
# Generic
sequence = fields.Integer(default=5,
help="When dealing with multiple actions, the execution order is "
@@ -458,15 +457,7 @@ class IrActionsServer(models.Model):
@api.model
def run_action_object_write(self, action, eval_context=None):
- """ Write server action.
-
- - 1. evaluate the value mapping
- - 2. depending on the write configuration:
-
- - `current`: id = active_id
- - `other`: id = from reference object
- - `expression`: id = from expression evaluation
- """
+ """Apply specified write changes to active_id."""
res = {}
for exp in action.fields_lines:
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
@@ -480,16 +471,9 @@ class IrActionsServer(models.Model):
@api.model
def run_action_object_create(self, action, eval_context=None):
- """ Create and Copy server action.
-
- - 1. evaluate the value mapping
- - 2. depending on the write configuration:
+ """Create specified model object with specified values.
- - `new`: new record in the base model
- - `copy_current`: copy the current record (id = active_id) + gives custom values
- - `new_other`: new record in target model
- - `copy_other`: copy the current record (id from reference object)
- + gives custom values
+ If applicable, link active_id.<self.link_field_id> to the new record.
"""
res = {}
for exp in action.fields_lines:
diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py
index c19a33c9b77..ccc34bd3dd1 100644
--- a/odoo/addons/base/models/ir_cron.py
+++ b/odoo/addons/base/models/ir_cron.py
@@ -228,6 +228,7 @@ class ir_cron(models.Model):
try:
registry = odoo.registry(db_name)
registry[cls._name]._process_job(job_cr, job, lock_cr)
+ _logger.info('Job `%s` done.', job['name'])
except Exception:
_logger.exception('Unexpected exception while processing cron job %r', job)
finally:
diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py
index 4572d2a2ba1..8cbb65b0600 100644
--- a/odoo/addons/base/models/res_config.py
+++ b/odoo/addons/base/models/res_config.py
@@ -527,19 +527,19 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
# Special case when value is the id of a deleted record, we do not want to
# block the settings screen
value = self.env[field.comodel_name].browse(int(value)).exists().id
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = False
elif field.type == 'integer':
try:
value = int(value)
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = 0
elif field.type == 'float':
try:
value = float(value)
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = 0.0
elif field.type == 'boolean':
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 2fb44ff6382..00705fb0018 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -997,23 +997,20 @@ class UsersImplied(models.Model):
# complete 'groups_id' with implied groups
user = self.new(values)
gs = user.groups_id._origin
- group_public = self.env.ref('base.group_public', raise_if_not_found=False)
- group_portal = self.env.ref('base.group_portal', raise_if_not_found=False)
- if group_public and group_public in gs:
- gs = group_public
- elif group_portal and group_portal in gs:
- gs = group_portal
gs = gs | gs.trans_implied_ids
values['groups_id'] = type(self).groups_id.convert_to_write(gs, user)
return super(UsersImplied, self).create(vals_list)
@api.multi
def write(self, values):
+ users_before = self.filtered(lambda u: u.has_group('base.group_user'))
res = super(UsersImplied, self).write(values)
if values.get('groups_id'):
# add implied groups for all users
for user in self:
- if not user.has_group('base.group_user'):
+ if not user.has_group('base.group_user') and user in users_before:
+ # if we demoted a user, we strip him of all its previous privileges
+ # (but we should not do it if we are simply adding a technical group to a portal user)
vals = {'groups_id': [(5, 0, 0)] + values['groups_id']}
super(UsersImplied, user).write(vals)
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py
index bba0d5b6742..6d1a02303a2 100644
--- a/odoo/addons/base/tests/__init__.py
+++ b/odoo/addons/base/tests/__init__.py
@@ -37,4 +37,3 @@ from . import test_res_partner
from . import test_res_partner_bank
from . import test_reports
from . import test_tests_tags
-from . import test_non_regression
diff --git a/odoo/addons/base/tests/test_non_regression.py b/odoo/addons/base/tests/test_non_regression.py
deleted file mode 100644
index 12faee0e1f0..00000000000
--- a/odoo/addons/base/tests/test_non_regression.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Non-Regression Tests
-"""
-
-from openerp.tests.common import TransactionCase
-
-
-class TestNR(TransactionCase):
- def test_issue26036(self):
- # Coming from https://github.com/odoo/odoo/pull/26036
- U = self.env["res.users"]
- G = self.env["res.groups"]
- group_user = self.env.ref('base.group_user')
- group_no_one = self.env.ref('base.group_no_one')
-
- group_A = G.create({"name": "A"})
- group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
- group_B = G.create({"name": "B"})
- group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
- group_C = G.create({"name": "C"})
-
- user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
- user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_BB.id])]})
-
- self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
-
- (user_a + user_b).write({"groups_id": [(4, group_C.id)]})
-
- self.assertEqual(user_a.groups_id, (group_AA + group_A + group_C + group_user + group_no_one))
- # As user_b is not an internal user, all its groups are removed
- self.assertEqual(user_b.groups_id, group_C)
diff --git a/odoo/addons/base/tests/test_user_has_group.py b/odoo/addons/base/tests/test_user_has_group.py
index 18326607f06..12c1be4a262 100644
--- a/odoo/addons/base/tests/test_user_has_group.py
+++ b/odoo/addons/base/tests/test_user_has_group.py
@@ -28,6 +28,8 @@ class TestHasGroup(TransactionCase):
self.grp_internal = self.env.ref(self.grp_internal_xml_id)
self.grp_portal_xml_id = 'base.group_portal'
self.grp_portal = self.env.ref(self.grp_portal_xml_id)
+ self.grp_public_xml_id = 'base.group_public'
+ self.grp_public = self.env.ref(self.grp_public_xml_id)
def test_env_uid(self):
Users = self.env['res.users'].sudo(self.test_user)
@@ -51,6 +53,10 @@ class TestHasGroup(TransactionCase):
)
def test_portal_creation(self):
+ """Here we check that portal user creation fails if it tries to create a user
+ who would also have group_user by implied_group.
+ Otherwise, it succeeds with the groups we asked for.
+ """
grp_public = self.env.ref('base.group_public')
grp_test_portal_xml_id = 'test_user_has_group.portal_implied_group'
grp_test_portal = self.env['res.groups']._load_records([
@@ -64,14 +70,16 @@ class TestHasGroup(TransactionCase):
dict(xml_id=grp_test_internal2_xml_id, values={'name': 'Test Group Internal 2'})
])
self.grp_portal.implied_ids = grp_test_portal
- grp_test_internal1.implied_ids = self.grp_internal
- grp_test_internal2.implied_ids = self.grp_internal
+
+ grp_test_internal1.implied_ids = False
+ grp_test_internal2.implied_ids = False
+
portal_user = self.env['res.users'].create({
'login': 'portalTest',
'name': 'Portal test',
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
- })
+ })
self.assertTrue(
portal_user.has_group(self.grp_portal_xml_id),
@@ -81,45 +89,52 @@ class TestHasGroup(TransactionCase):
portal_user.has_group(grp_test_portal_xml_id),
"The portal user should belong to '%s'" % grp_test_portal_xml_id,
)
- self.assertFalse(
+ self.assertTrue(
portal_user.has_group(grp_test_internal2_xml_id),
- "The portal user should not belong to '%s'" % grp_test_internal2_xml_id
+ "The portal user should belong to '%s'" % grp_test_internal2_xml_id
)
self.assertFalse(
portal_user.has_group(self.grp_internal_xml_id),
"The portal user should not belong to '%s'" % self.grp_internal_xml_id
)
+ portal_user.unlink() # otherwise, badly modifying the implication would raise
+
+ grp_test_internal1.implied_ids = self.grp_internal
+ grp_test_internal2.implied_ids = self.grp_internal
+
+ with self.assertRaises(ValidationError): # current group implications forbid to create a portal user
+ portal_user = self.env['res.users'].create({
+ 'login': 'portalFail',
+ 'name': 'Portal fail',
+ 'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
+ 'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
+ })
+
def test_portal_write(self):
- grp_remove_xml_id = 'test_portal_write.group_to_remove'
- grp_remove = self.env['res.groups']._load_records([
- dict(xml_id=grp_remove_xml_id, values={'name': 'Group to remove'})
- ])
+ """Check that adding a new group to a portal user works as expected,
+ except if it implies group_user/public, in chich case it should raise.
+ """
+ grp_test_portal = self.env["res.groups"].create({"name": "implied by portal"})
+ self.grp_portal.implied_ids = grp_test_portal
+
portal_user = self.env['res.users'].create({
'login': 'portalTest2',
'name': 'Portal test 2',
- 'groups_id': [(6, 0, [grp_remove.id])],
+ 'groups_id': [(6, 0, [self.grp_portal.id])],
})
- grp_test_portal_xml_id = 'test_portal_write.portal_implied_group'
- grp_test_portal = self.env['res.groups']._load_records([
- dict(xml_id=grp_test_portal_xml_id, values={'name': 'Test Group Portal'})
- ])
- self.grp_portal.implied_ids = grp_test_portal
- portal_user.write({'groups_id': [(4, self.grp_portal.id, 0)]})
- self.assertFalse(
- portal_user.has_group(grp_remove_xml_id),
- "The portal user should not belong to '%s'" % grp_remove_xml_id
- )
- self.assertTrue(
- portal_user.has_group(self.grp_portal_xml_id),
- "The portal user should belong to '%s'" % self.grp_portal_xml_id,
- )
- self.assertTrue(
- portal_user.has_group(grp_test_portal_xml_id),
- "The portal user should belong to '%s'" % grp_test_portal_xml_id,
+ self.assertEqual(
+ portal_user.groups_id, (self.grp_portal + grp_test_portal),
+ "The portal user should have the implied group.",
)
+ grp_fail = self.env["res.groups"].create(
+ {"name": "fail", "implied_ids": [(6, 0, [self.grp_internal.id])]})
+
+ with self.assertRaises(ValidationError):
+ portal_user.write({'groups_id': [(4, grp_fail.id)]})
+
def test_two_user_types(self):
#Create a user with two groups of user types kind (Internal and Portal)
grp_test = self.env['res.groups']._load_records([
@@ -161,3 +176,76 @@ class TestHasGroup(TransactionCase):
with self.assertRaises(ValidationError):
grp_test.write({'implied_ids': [(4, self.grp_portal.id)]})
+
+ def test_demote_user(self):
+ """When a user is demoted to the status of portal/public,
+ we should strip him of all his (previous) rights
+ """
+ group_0 = self.env.ref(self.group0) # the group to which test_user already belongs
+ group_U = self.env["res.groups"].create({"name": "U", "implied_ids": [(6, 0, [self.grp_internal.id])]})
+ self.grp_internal.implied_ids = False # only there to simplify the test by not having to care about its trans_implied_ids
+
+ self.test_user.write({'groups_id': [(4, group_U.id)]})
+ self.assertEqual(
+ self.test_user.groups_id, (group_0 + group_U + self.grp_internal),
+ "We should have our 2 groups and the implied user group",
+ )
+
+ # Now we demote him. The JS framework sends 3 and 4 commands,
+ # which is what we write here, but it should work even with a 5 command or whatever.
+ self.test_user.write({'groups_id': [
+ (3, self.grp_internal.id),
+ (3, self.grp_public.id),
+ (4, self.grp_portal.id),
+ ]})
+
+ # if we screw up the removing groups/adding the implied ids, we could end up in two situations:
+ # 1. we have a portal user with way too much rights (e.g. 'Contact Creation', which does not imply any other group)
+ # 2. because a group may be (transitively) implying group_user, then it would raise an exception
+ # so as a compromise we remove all groups when demoting a user
+ # (even technical display groups, e.g. TaxB2B, which could be re-added later)
+ self.assertEqual(
+ self.test_user.groups_id, (self.grp_portal),
+ "Here the portal group does not imply any other group, so we should only have this group.",
+ )
+
+ def test_implied_groups(self):
+ """ We check that the adding of implied ids works correctly for normal users and portal users.
+ In the second case, working normally means raising if a group implies to give 'group_user'
+ rights to a portal user.
+ """
+ U = self.env["res.users"]
+ G = self.env["res.groups"]
+ group_user = self.env.ref('base.group_user')
+ group_portal = self.env.ref('base.group_portal')
+ group_no_one = self.env.ref('base.group_no_one')
+
+ group_A = G.create({"name": "A"})
+ group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
+ group_B = G.create({"name": "B"})
+ group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
+
+ # user_a is a normal user, so we expect groups to be added when we add them,
+ # as well as 'implied_groups'; otherwise nothing else should happen.
+ # By contrast, for a portal user we want implied groups not to be added
+ # if and only if it would not give group_user (or group_public) privileges
+ user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
+
+ user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_portal.id, group_AA.id])]})
+ self.assertEqual(user_b.groups_id, (group_AA + group_A + group_portal))
+
+ # user_b is not an internal user, but giving it a new group just added a new group
+ (user_a + user_b).write({"groups_id": [(4, group_BB.id)]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_user + group_no_one))
+ self.assertEqual(user_b.groups_id, (group_AA + group_A + group_BB + group_B + group_portal))
+
+ # now we create a group that implies the group_user
+ # adding it to a user should work normally, whereas adding it to a portal user should raise
+ group_C = G.create({"name": "C", "implied_ids": [(6, 0, [group_user.id])]})
+
+ user_a.write({"groups_id": [(4, group_C.id)]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_C + group_user + group_no_one))
+
+ with self.assertRaises(ValidationError):
+ user_b.write({"groups_id": [(4, group_C.id)]})
diff --git a/odoo/service/model.py b/odoo/service/model.py
index ca997d0f730..e5002f5e350 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -161,6 +161,7 @@ def check(f):
return wrapper
def execute_cr(cr, uid, obj, method, *args, **kw):
+ odoo.api.Environment.reset() # clean cache etc if we retry the same transaction
recs = odoo.api.Environment(cr, uid, {}).get(obj)
if recs is None:
raise UserError(_("Object %s doesn't exist") % obj)
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 207d590e724..e6ad74bd027 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -507,6 +507,8 @@ class ThreadedServer(CommonServer):
# We wait there is no processing requests
# other than the ones exceeding the limits, up to 1 min,
# before asking for a reload.
+ _logger.info('Dumping stacktrace of limit exceeding threads before reloading')
+ dumpstacks(thread_idents=[thread.ident for thread in self.limits_reached_threads])
self.reload()
# `reload` increments `self.quit_signals_received`
# and the loop will end after this iteration,
@@ -685,7 +687,7 @@ class PreforkServer(CommonServer):
raise KeyboardInterrupt
elif sig == signal.SIGQUIT:
# dump stacks on kill -3
- self.dumpstacks()
+ dumpstacks()
elif sig == signal.SIGUSR1:
# log ormcache stats on kill -SIGUSR1
log_ormcache_stats()
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 44c3dd4bc41..a3832bef6c2 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -895,8 +895,10 @@ class ConstantMapping(Mapping):
return self._value
-def dumpstacks(sig=None, frame=None):
- """ Signal handler: dump a stack trace for each existing thread."""
+def dumpstacks(sig=None, frame=None, thread_idents=None):
+ """ Signal handler: dump a stack trace for each existing thread or given
+ thread(s) specified through the ``thread_idents`` sequence.
+ """
code = []
def extract_stack(stack):
@@ -913,14 +915,15 @@ def dumpstacks(sig=None, frame=None):
'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
for threadId, stack in sys._current_frames().items():
- thread_info = threads_info.get(threadId, {})
- code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
- (thread_info.get('repr', threadId),
- thread_info.get('dbname', 'n/a'),
- thread_info.get('uid', 'n/a'),
- thread_info.get('url', 'n/a')))
- for line in extract_stack(stack):
- code.append(line)
+ if not thread_idents or threadId in thread_idents:
+ thread_info = threads_info.get(threadId, {})
+ code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
+ (thread_info.get('repr', threadId),
+ thread_info.get('dbname', 'n/a'),
+ thread_info.get('uid', 'n/a'),
+ thread_info.get('url', 'n/a')))
+ for line in extract_stack(stack):
+ code.append(line)
if odoo.evented:
# code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets