diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-23 17:40:06 +0000 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-24 09:44:56 +0000 |
| commit | ac4fc6e7c7d71c3f8e2440a8ed8095c5aab15dd8 (patch) | |
| tree | e143d6d261f22cb06f2c0d5b344a4353e5b3be1b /addons | |
| parent | a6a3df3d0eda0fc2476c59de61d59534815f7c20 (diff) | |
| parent | bd3e510edbcd62f508dae28f573ee4d1a51bcf87 (diff) | |
[MERGE] forward port branch saas-12.3 up to bd3e510edbc
closes odoo/odoo#37314
Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'addons')
63 files changed, 2506 insertions, 395 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 |
