diff options
| author | Olivier Dony <odo@openerp.com> | 2017-06-01 01:46:13 +0200 |
|---|---|---|
| committer | Olivier Dony <odo@openerp.com> | 2017-06-01 01:46:13 +0200 |
| commit | 5d2869cbc812f716e3115b5efb9bd1c3082f47b7 (patch) | |
| tree | 364844fc326f0d095997120d10d83e67fdf341e9 /addons | |
| parent | 04111cae39e36015d3c8494f52b110ab5ab6ad80 (diff) | |
| parent | ba15df47cb25625cb0921cba554aa0879048c90a (diff) | |
[MERGE] Forward-port saas-16 up to ba15df47cb25625cb0921cba554aa0879048c90a
Diffstat (limited to 'addons')
171 files changed, 4783 insertions, 1278 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 1856c526d0f..96ade1cb3d9 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -520,6 +520,12 @@ class AccountJournal(models.Model): res += [(journal.id, name)] return res + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + args = args or [] + recs = self.search(['|', ('code', operator, name), ('name', operator, name)] + args, limit=limit) + return recs.name_get() + @api.multi @api.depends('company_id') def _belong_to_company(self): diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 26f09500b76..6c728362378 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -410,8 +410,6 @@ class AccountBankStatementLine(models.Model): for line in self: if line.journal_entry_ids.ids: raise UserError(_('In order to delete a bank statement line, you must first cancel it to delete related journal items.')) - if line.move_name: - raise UserError(_('It is not allowed to delete a bank statement line that already created a journal entry since it would create a gap in the numbering. You should create the journal entry again and cancel it thanks to a regular revert.')) return super(AccountBankStatementLine, self).unlink() @api.multi diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 8c0d227e02d..e8c0041068e 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -486,6 +486,8 @@ class AccountInvoice(models.Model): payment_term_id = False fiscal_position = False bank_id = False + warning = {} + domain = {} company_id = self.company_id.id p = self.partner_id if not company_id else self.partner_id.with_context(force_company=company_id) type = self.type @@ -520,7 +522,6 @@ class AccountInvoice(models.Model): } if p.invoice_warn == 'block': self.partner_id = False - return {'warning': warning} self.account_id = account_id self.payment_term_id = payment_term_id @@ -530,8 +531,14 @@ class AccountInvoice(models.Model): bank_ids = p.commercial_partner_id.bank_ids bank_id = bank_ids[0].id if bank_ids else False self.partner_bank_id = bank_id - return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} - return {} + domain = {'partner_bank_id': [('id', 'in', bank_ids.ids)]} + + res = {} + if warning: + res['warning'] = warning + if domain: + res['domain'] = domain + return res @api.multi def get_delivery_partner_id(self): @@ -1029,6 +1036,20 @@ class AccountInvoice(models.Model): result.append((0, 0, values)) return result + def _get_refund_common_fields(self): + return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id'] + + def _get_refund_prepare_fields(self): + return ['name', 'reference', 'comment', 'date_due'] + + def _get_refund_modify_read_fields(self): + read_fields = ['type', 'number', 'invoice_line_ids', 'tax_line_ids', 'date'] + return self._get_refund_common_fields() + self._get_refund_prepare_fields() + read_fields + + def _get_refund_copy_fields(self): + copy_fields = ['company_id', 'user_id', 'fiscal_position_id'] + return self._get_refund_common_fields() + self._get_refund_prepare_fields() + copy_fields + @api.model def _get_refund_common_fields(self): return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id'] @@ -1397,6 +1418,12 @@ class AccountInvoiceLine(models.Model): """ pass + @api.multi + def unlink(self): + if self.filtered(lambda r: r.invoice_id and r.invoice_id.state != 'draft'): + raise UserError(_('You can only delete an invoice line if the invoice is in draft state.')) + return super(AccountInvoiceLine, self).unlink() + class AccountInvoiceTax(models.Model): _name = "account.invoice.tax" _description = "Invoice Tax" diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7aad4d9a87e..dc5e0067791 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -228,8 +228,8 @@ class AccountMove(models.Model): 'date': date, 'journal_id': journal_id.id if journal_id else self.journal_id.id, 'ref': _('reversal of: ') + self.name}) - for acm_line in reversed_move.line_ids: - acm_line.with_context(check_move_validity=False).write({ + for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): + acm_line.write({ 'debit': acm_line.credit, 'credit': acm_line.debit, 'amount_currency': -acm_line.amount_currency diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 81d7d07a0de..f4ee2a4b4f1 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -52,7 +52,7 @@ class account_abstract_payment(models.AbstractModel): @api.constrains('amount') def _check_amount(self): if not self.amount > 0.0: - raise ValidationError('The payment amount must be strictly positive.') + raise ValidationError(_('The payment amount must be strictly positive.')) @api.one @api.depends('payment_type', 'journal_id') @@ -434,8 +434,16 @@ class account_payment(models.Model): amount_wo = total_payment_company_signed - total_residual_company_signed else: amount_wo = total_residual_company_signed - total_payment_company_signed - debit_wo = amount_wo > 0 and amount_wo or 0.0 - credit_wo = amount_wo < 0 and -amount_wo or 0.0 + # Align the sign of the secondary currency writeoff amount with the sign of the writeoff + # amount in the company currency + if amount_wo > 0: + debit_wo = amount_wo + credit_wo = 0.0 + amount_currency_wo = abs(amount_currency_wo) + else: + debit_wo = 0.0 + credit_wo = -amount_wo + amount_currency_wo = -abs(amount_currency_wo) writeoff_line['name'] = self.writeoff_label writeoff_line['account_id'] = self.writeoff_account_id.id writeoff_line['debit'] = debit_wo diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 2bcddda579c..619476fb240 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -322,7 +322,7 @@ <field domain="[('company_id', '=', company_id), ('internal_type', '=', 'payable'), ('deprecated', '=', False)]" name="account_id" groups="account.group_account_manager"/> <field name="name" invisible="1"/> - <field name="payment_term_id" options="{'no_create': True}" attrs="{'invisible': [('payment_term_id','=',False)]}"/> + <field name="payment_term_id" options="{'no_create': True}"/> </group> <group> <field name="fiscal_position_id" options="{'no_create': True}" placeholder="Auto-detect"/> diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 6ce58693f2d..eee2bd92660 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -171,7 +171,7 @@ id="account.action_all_partner_invoices" name="All Invoices" res_model="account.invoice" - domain="[('partner_id', '=', active_id)]" + domain="[('partner_id', 'child_of', active_id)]" src_model="res.partner"/> <act_window @@ -226,6 +226,7 @@ <record id="view_account_journal_tree" model="ir.ui.view"> <field name="name">account.journal.tree</field> <field name="model">account.journal</field> + <field name="priority">1</field> <field name="arch" type="xml"> <tree string="Account Journal"> <field name='sequence' widget='handle'/> diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index 4923ef95040..f0840aa7856 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -95,7 +95,10 @@ class AccountInvoiceRefund(models.TransientModel): 'fiscal_position_id': inv.fiscal_position_id.id, }) for field in inv_obj._get_refund_common_fields(): - invoice[field] = invoice[field] and invoice[field][0] + if inv_obj._fields[field].type == 'many2one': + invoice[field] = invoice[field] and invoice[field][0] + else: + invoice[field] = invoice[field] or False inv_refund = inv_obj.create(invoice) if inv_refund.payment_term_id.id: inv_refund._onchange_payment_term_date_invoice() diff --git a/addons/account_asset/static/tests/account_asset_tests.js b/addons/account_asset/static/tests/account_asset_tests.js index eab7f5f5d99..8f353f3bf5c 100644 --- a/addons/account_asset/static/tests/account_asset_tests.js +++ b/addons/account_asset/static/tests/account_asset_tests.js @@ -147,7 +147,7 @@ QUnit.test('click events are correctly triggered', function (assert) { execute_action: function (event) { var data = event.data; assert.strictEqual(data.model, 'line', "should have correct model"); - assert.strictEqual(data.record_id, 4, "should have correct res_id"); + assert.deepEqual(data.res_ids, [4], "should have correct res_ids"); assert.strictEqual(data.action_data.name, 'create_move', "should call correct method"); }, diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py index 2d45a95612d..07ab4c7dc6e 100644 --- a/addons/account_bank_statement_import/account_bank_statement_import.py +++ b/addons/account_bank_statement_import/account_bank_statement_import.py @@ -43,6 +43,8 @@ class AccountBankStatementImport(models.TransientModel): if not journal: # The active_id is passed in context so the wizard can call import_file again once the journal is created return self.with_context(active_id=self.ids[0])._journal_creation_wizard(currency, account_number) + if not journal.default_debit_account_id or not journal.default_credit_account_id: + raise UserError(_('You have to set a Default Debit Account and a Default Credit Account for the journal: %s') % (journal.name,)) # Prepare statement data to be used for bank statements creation stmts_vals = self._complete_stmts_vals(stmts_vals, journal, account_number) # Create the bank statements @@ -220,6 +222,9 @@ class AccountBankStatementImport(models.TransientModel): filtered_st_lines.append(line_vals) else: ignored_statement_lines_import_ids.append(line_vals['unique_import_id']) + if 'balance_start' in st_vals: + st_vals['balance_start'] += line_vals['amount'] + if len(filtered_st_lines) > 0: # Remove values that won't be used to create records st_vals.pop('transactions', None) diff --git a/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot b/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot index 43cc6e38d59..32c6952c711 100644 --- a/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot +++ b/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot @@ -469,6 +469,12 @@ msgid "You have already imported that file." msgstr "" #. module: account_bank_statement_import +#: code:addons/account_bank_statement_import/account_bank_statement_import.py:47 +#, python-format +msgid "You have to set a Default Debit Account and a Default Credit Account for the journal: %s" +msgstr "" + +#. module: account_bank_statement_import #: model:ir.ui.view,arch_db:account_bank_statement_import.account_bank_statement_import_view msgid "_Import" msgstr "" diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 13b9e39cb22..ef33dc05ae4 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -29,13 +29,7 @@ class AccountRegisterPayments(models.TransientModel): def _onchange_amount(self): if hasattr(super(AccountRegisterPayments, self), '_onchange_amount'): super(AccountRegisterPayments, self)._onchange_amount() - # TODO: merge, refactor and complete the amount_to_text and amount_to_text_en classes - check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(self.amount), lang='en', currency='') - check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh - decimals = self.amount % 1 - if decimals >= 10**-2: - check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) - self.check_amount_in_words = check_amount_in_words + self.check_amount_in_words = self.env['account.payment']._get_check_amount_in_words(self.amount) def get_payment_vals(self): res = super(AccountRegisterPayments, self).get_payment_vals() @@ -56,6 +50,15 @@ class AccountPayment(models.Model): help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers " "or if the current numbering is wrong, you can change it in the journal configuration page.") + def _get_check_amount_in_words(self, amount): + # TODO: merge, refactor and complete the amount_to_text and amount_to_text_en classes + check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(amount), lang='en', currency='') + check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh + decimals = amount % 1 + if decimals >= 10**-2: + check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) + return check_amount_in_words + @api.onchange('journal_id') def _onchange_journal_id(self): if hasattr(super(AccountPayment, self), '_onchange_journal_id'): @@ -67,12 +70,7 @@ class AccountPayment(models.Model): def _onchange_amount(self): if hasattr(super(AccountPayment, self), '_onchange_amount'): super(AccountPayment, self)._onchange_amount() - check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(self.amount), lang='en', currency='') - check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh - decimals = self.amount % 1 - if decimals >= 10**-2: - check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) - self.check_amount_in_words = check_amount_in_words + self.check_amount_in_words = self._get_check_amount_in_words(self.amount) def _check_communication(self, payment_method_id, communication): super(AccountPayment, self)._check_communication(payment_method_id, communication) diff --git a/addons/analytic/views/analytic_account_views.xml b/addons/analytic/views/analytic_account_views.xml index 83ef8ae2347..512b304ccfd 100644 --- a/addons/analytic/views/analytic_account_views.xml +++ b/addons/analytic/views/analytic_account_views.xml @@ -51,7 +51,7 @@ </record> <record model="ir.actions.act_window" id="account_analytic_line_action"> - <field name="context">{'search_default_group_date': 1, 'search_default_group_journal': 1, 'default_account_id': active_id}</field> + <field name="context">{'search_default_group_date': 1, 'default_account_id': active_id}</field> <field name="domain">[('account_id','=', active_id)]</field> <field name="name">Costs & Revenues</field> <field name="res_model">account.analytic.line</field> diff --git a/addons/barcodes/models/barcodes.py b/addons/barcodes/models/barcodes.py index 08482581bb5..6c3d228895e 100644 --- a/addons/barcodes/models/barcodes.py +++ b/addons/barcodes/models/barcodes.py @@ -44,8 +44,8 @@ class BarcodeNomenclature(models.Model): if len(code) != 8: return -1 - sum1 = ean[1] + ean[3] + ean[5] - sum2 = ean[0] + ean[2] + ean[4] + ean[6] + sum1 = int(ean[1]) + int(ean[3]) + int(ean[5]) + sum2 = int(ean[0]) + int(ean[2]) + int(ean[4]) + int(ean[6]) total = sum1 + 3 * sum2 return int((10 - total % 10) % 10) diff --git a/addons/barcodes/tests/__init__.py b/addons/barcodes/tests/__init__.py new file mode 100644 index 00000000000..6188e48fb99 --- /dev/null +++ b/addons/barcodes/tests/__init__.py @@ -0,0 +1 @@ +from . import test_barcode_nomenclature diff --git a/addons/barcodes/tests/test_barcode_nomenclature.py b/addons/barcodes/tests/test_barcode_nomenclature.py new file mode 100644 index 00000000000..d3ef2903d79 --- /dev/null +++ b/addons/barcodes/tests/test_barcode_nomenclature.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo.tests import common + + +class TestBarcodeNomenclature(common.TransactionCase): + + def test_ean8_checksum(self): + barcode_nomenclature = self.env['barcode.nomenclature'] + ean8 = "87111125" + checksum = barcode_nomenclature.ean8_checksum(ean8) + self.assertEqual(checksum, int(ean8[-1])) + checksum = barcode_nomenclature.ean8_checksum("8711112") + self.assertEqual(checksum, -1) + checksum = barcode_nomenclature.ean8_checksum("871111256") + self.assertEqual(checksum, -1) diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py index 8c57a0cf3c4..c83238088e2 100644 --- a/addons/base_vat/models/res_partner.py +++ b/addons/base_vat/models/res_partner.py @@ -223,10 +223,14 @@ class ResPartner(models.Model): return True # Norway VAT validation, contributed by Rolv Råen (adEgo) <rora@adego.no> + # Support for MVA suffix contributed by Bringsvor Consulting AS (bringsvor@bringsvor.com) def check_vat_no(self, vat): - ''' + """ Check Norway VAT number.See http://www.brreg.no/english/coordination/number.html - ''' + """ + if len(vat) == 12 and vat.upper().endswith('MVA'): + vat = vat[:-3] # Strictly speaking we should enforce the suffix MVA but... + if len(vat) != 9: return False try: diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index acf81039dbf..98d169c11e2 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -279,26 +279,27 @@ FormRenderer.include({ children: [] }); } - var $html = $('<div>').append($(QWeb.render('DashBoard', {node: node}))); - // render each view + // register actions, alongside a generated unique ID _.each(node.children, function (column, column_index) { _.each(column.children, function (action, action_index) { - var attrs = action.attrs; - var domain = Domain.prototype.stringToArray(attrs.domain, {}); - var context = new Context(attrs.context); - var actionID = _.str.toNumber(attrs.name); - self.actionsDescr[actionID] = attrs; - var $node = $html.find('.oe_action[data-id=' + actionID + '] .oe_content'); - self.defs.push(self._createController({ - $node: $node, - actionID: actionID, - context: context, - domain: domain, - viewType: attrs.view_mode, - })); + action.attrs.id = 'action_' + column_index + '_' + action_index; + self.actionsDescr[action.attrs.id] = action.attrs; }); }); + + var $html = $('<div>').append($(QWeb.render('DashBoard', {node: node}))); + + // render each view + _.each(this.actionsDescr, function (action) { + self.defs.push(self._createController({ + $node: $html.find('.oe_action[data-id=' + action.id + '] .oe_content'), + actionID: _.str.toNumber(action.name), + context: new Context(action.context), + domain: Domain.prototype.stringToArray(action.domain, {}), + viewType: action.view_mode, + })); + }); $html.find('.oe_dashboard_column').sortable({ connectWith: '.oe_dashboard_column', handle: '.oe_header', @@ -341,7 +342,7 @@ FormRenderer.include({ */ _onFoldClick: function (event) { var $e = $(event.currentTarget); - var $action = $e.parents('.oe_action:first'); + var $action = $e.closest('.oe_action'); var id = $action.data('id'); var actionAttrs = this.actionsDescr[id]; diff --git a/addons/board/static/src/less/dashboard.less b/addons/board/static/src/less/dashboard.less index 72131e2fa2e..5b53c583604 100644 --- a/addons/board/static/src/less/dashboard.less +++ b/addons/board/static/src/less/dashboard.less @@ -108,6 +108,9 @@ } .oe_content { padding: 0 12px 12px 12px; + &.oe_folded { + display: none; + } } // Override border of many2manytags defined for form_views @@ -139,4 +142,5 @@ // Favorites menu in control panel .o_add_to_dashboard { display: none; // hidden by default + .o-search-options-dropdown-custom-li; } diff --git a/addons/board/static/src/xml/board.xml b/addons/board/static/src/xml/board.xml index 1c40084d13c..13453a945f6 100644 --- a/addons/board/static/src/xml/board.xml +++ b/addons/board/static/src/xml/board.xml @@ -18,7 +18,7 @@ </table> </t> <t t-name="DashBoard.action"> - <div t-att-data-id="action.attrs.name" class="oe_action"> + <div t-att-data-id="action.attrs.id" class="oe_action"> <h2 t-attf-class="oe_header #{action.attrs.string ? '' : 'oe_header_empty'}"> <span class="oe_header_txt"> <t t-esc="action.attrs.string"/> </span> <input class = "oe_header_text" type="text"/> @@ -27,8 +27,7 @@ <span class='oe_icon oe_minimize oe_fold' t-if="!action.attrs.fold"></span> <span class='oe_icon oe_maximize oe_fold' t-if="action.attrs.fold"></span> </h2> - - <div t-attf-id="#{element_id}_action_#{column_index}_#{action_index}" class="oe_content" t-att-style="action.attrs.fold ? 'display: none' : null"></div> + <div t-att-class="'oe_content' + (action.attrs.fold ? ' oe_folded' : '')"/> </div> </t> <t t-name="DashBoard.layouts"> diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js index d921cd56a67..1b55551fe2b 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -326,4 +326,55 @@ QUnit.test('can drag and drop a view', function (assert) { form.destroy(); }); +QUnit.test('twice the same action in a dashboard', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'board', + data: this.data, + arch: '<form string="My Dashboard">' + + '<board style="2-1">' + + '<column>' + + '<action context="{}" view_mode="list" string="ABC" name="51" domain="[]"></action>' + + '<action context="{}" view_mode="kanban" string="DEF" name="51" domain="[]"></action>' + + '</column>' + + '</board>' + + '</form>', + mockRPC: function (route) { + if (route === '/board/static/src/img/layout_1-1-1.png') { + return $.when(); + } + if (route === '/web/action/load') { + return $.when({ + res_model: 'partner', + views: [[4, 'list'],[5, 'kanban']], + }); + } + if (route === '/web/view/add_custom') { + assert.step('add custom'); + return $.when(true); + } + return this._super.apply(this, arguments); + }, + archs: { + 'partner,4,list': + '<tree string="Partner"><field name="foo"/></tree>', + 'partner,5,kanban': + '<kanban><templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates></kanban>', + }, + }); + + var $firstAction = form.$('.oe_action:contains(ABC)'); + assert.strictEqual($firstAction.find('.o_list_view').length, 1, + "list view should be displayed in 'ABC' block"); + var $secondAction = form.$('.oe_action:contains(DEF)'); + assert.strictEqual($secondAction.find('.o_kanban_view').length, 1, + "kanban view should be displayed in 'DEF' block"); + + form.destroy(); +}); + }); diff --git a/addons/calendar/models/mail_message.py b/addons/calendar/models/mail_message.py index 421b070dcb7..0dc4d24e364 100644 --- a/addons/calendar/models/mail_message.py +++ b/addons/calendar/models/mail_message.py @@ -25,7 +25,7 @@ class Message(models.Model): @api.model def _find_allowed_model_wise(self, doc_model, doc_dict): if doc_model == 'calendar.event': - order = self._context.get('order', self._order) + order = self._context.get('order', self.env[doc_model]._order) for virtual_id in self.env[doc_model].browse(doc_dict).get_recurrent_ids([], order=order): doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)]) return super(Message, self)._find_allowed_model_wise(doc_model, doc_dict) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 8ca20f7a84b..7d55a0805e5 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -97,7 +97,7 @@ class Lead(models.Model): user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', default=lambda self: self.env.user) referred = fields.Char('Referred By') - date_open = fields.Datetime('Assigned', readonly=True) + date_open = fields.Datetime('Assigned', readonly=True, default=fields.Datetime.now) day_open = fields.Float(compute='_compute_day_open', string='Days to Assign', store=True) day_close = fields.Float(compute='_compute_day_close', string='Days to Close', store=True) date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, default=fields.Datetime.now) @@ -685,7 +685,7 @@ class Lead(models.Model): email_split = tools.email_split(self.email_from) return { 'name': name, - 'user_id': self.user_id.id, + 'user_id': self.env.context.get('default_user_id') or self.user_id.id, 'comment': self.description, 'team_id': self.team_id.id, 'parent_id': parent_id, @@ -891,29 +891,30 @@ class Lead(models.Model): 'nb_opportunities': 0, } - opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid), ('activity_date_deadline', '!=', False)]) + opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)]) for opp in opportunities: # Expected closing - if opp.date_deadline: - date_deadline = fields.Date.from_string(opp.date_deadline) - if date_deadline == date.today(): - result['closing']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): - result['closing']['next_7_days'] += 1 - if date_deadline < date.today(): - result['closing']['overdue'] += 1 - # Next activities - for activity in opp.activity_ids: - date_deadline = fields.Date.from_string(activity.date_deadline) - if date_deadline == date.today(): - result['activity']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): - result['activity']['next_7_days'] += 1 - if date_deadline < date.today(): - result['activity']['overdue'] += 1 + if opp.activity_date_deadline: + if opp.date_deadline: + date_deadline = fields.Date.from_string(opp.date_deadline) + if date_deadline == date.today(): + result['closing']['today'] += 1 + if date.today() <= date_deadline <= date.today() + timedelta(days=7): + result['closing']['next_7_days'] += 1 + if date_deadline < date.today() and not opp.date_closed: + result['closing']['overdue'] += 1 + # Next activities + for activity in opp.activity_ids: + date_deadline = fields.Date.from_string(activity.date_deadline) + if date_deadline == date.today(): + result['activity']['today'] += 1 + if date.today() <= date_deadline <= date.today() + timedelta(days=7): + result['activity']['next_7_days'] += 1 + if date_deadline < date.today(): + result['activity']['overdue'] += 1 # Won in Opportunities - if opp.date_closed: + if opp.date_closed and opp.stage_id.probability == 100: date_closed = fields.Date.from_string(opp.date_closed) if date.today().replace(day=1) <= date_closed <= date.today(): if opp.planned_revenue: diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py index d83af136d50..0af455f6bdc 100644 --- a/addons/crm/models/res_config.py +++ b/addons/crm/models/res_config.py @@ -60,7 +60,10 @@ class CRMSettings(models.TransientModel): @api.onchange('default_generate_lead_from_alias') def _onchange_default_generate_lead_from_alias(self): - self.alias_prefix = 'info' if self.default_generate_lead_from_alias else False + if self.default_generate_lead_from_alias: + self.alias_prefix = self.alias_prefix or 'info' + else: + self.alias_prefix = False @api.model def get_default_alias_prefix(self, fields): diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 4a46498c881..5f7442002a6 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -95,7 +95,9 @@ class Lead2OpportunityPartner(models.TransientModel): leads = self.env['crm.lead'].browse(vals.get('lead_ids')) for lead in leads: - partner_id = self._create_partner(lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) + self_def_user = self.with_context(default_user_id=self.user_id.id) + partner_id = self_def_user._create_partner( + lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) res = lead.convert_opportunity(partner_id, [], False) user_ids = vals.get('user_ids') @@ -135,9 +137,6 @@ class Lead2OpportunityPartner(models.TransientModel): leads = self.env['crm.lead'].browse(self._context.get('active_ids', [])) values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]}) self._convert_opportunity(values) - for lead in leads: - if lead.partner_id and lead.partner_id.user_id != lead.user_id: - self.env['res.partner'].browse(lead.partner_id.id).write({'user_id': lead.user_id.id}) return leads[0].redirect_opportunity_view() diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index ec9bbe2ffae..44801880267 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -10,7 +10,8 @@ class SaleOrder(models.Model): def action_confirm(self): self.ensure_one() res = super(SaleOrder, self).action_confirm() - self.order_line._update_registrations(confirm=False, cancel_to_draft=False) + # confirm registration if it was free (otherwise it will be confirmed once invoice fully paid) + self.order_line._update_registrations(confirm=self.amount_total == 0, cancel_to_draft=False) if any(self.order_line.filtered(lambda line: line.event_id)): return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration') return res diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 24a14c5e9f4..ffe7e8ed415 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -461,7 +461,6 @@ <field name="state" invisible="1"/> <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> <field name="message_unread" invisible="1"/> - <field name="attachment_number" invisible="1"/> <field name="attachment_number" string=" "/> <button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/> <field name="tax_ids" widget="many2many_tags"/> diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.py b/addons/hr_expense/wizard/hr_expense_register_payment.py index c8677b3bc35..b608b234144 100644 --- a/addons/hr_expense/wizard/hr_expense_register_payment.py +++ b/addons/hr_expense/wizard/hr_expense_register_payment.py @@ -32,7 +32,7 @@ class HrExpenseRegisterPaymentWizard(models.TransientModel): @api.constrains('amount') def _check_amount(self): if not self.amount > 0.0: - raise ValidationError('The payment amount must be strictly positive.') + raise ValidationError(_('The payment amount must be strictly positive.')) @api.one @api.depends('journal_id') diff --git a/addons/hr_expense_check/__init__.py b/addons/hr_expense_check/__init__.py new file mode 100644 index 00000000000..bff786c0885 --- /dev/null +++ b/addons/hr_expense_check/__init__.py @@ -0,0 +1 @@ +import models diff --git a/addons/hr_expense_check/__manifest__.py b/addons/hr_expense_check/__manifest__.py new file mode 100644 index 00000000000..90320ac6f64 --- /dev/null +++ b/addons/hr_expense_check/__manifest__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Check Printing in Expenses", + 'summary': """Print amount in words on checks issued for expenses""", + 'category': 'Accounting', + 'description': """ + Print amount in words on checks issued for expenses + """, + 'category': 'Accounting & Finance', + 'version': '1.0', + 'depends': ['account_check_printing', 'hr_expense'], + 'auto_install': True, + 'data': [ + 'views/payment.xml', + ], +} diff --git a/addons/hr_expense_check/models/__init__.py b/addons/hr_expense_check/models/__init__.py new file mode 100644 index 00000000000..d33d6b388d7 --- /dev/null +++ b/addons/hr_expense_check/models/__init__.py @@ -0,0 +1 @@ +import payment diff --git a/addons/hr_expense_check/models/payment.py b/addons/hr_expense_check/models/payment.py new file mode 100644 index 00000000000..e5041d05dc9 --- /dev/null +++ b/addons/hr_expense_check/models/payment.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class HrExpenseRegisterPaymentWizard(models.TransientModel): + _inherit = "hr.expense.register.payment.wizard" + + check_amount_in_words = fields.Char(string="Amount in Words") + check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing') + # Note: a check_number == 0 means that it will be attributed when the check is printed + check_number = fields.Integer(string="Check Number", readonly=True, copy=False, default=0, + help="Number of the check corresponding to this payment. If your pre-printed check are not already numbered, " + "you can manage the numbering in the journal configuration page.") + payment_method_code_2 = fields.Char(related='payment_method_id.code', + help="Technical field used to adapt the interface to the payment type selected.", + readonly=True) + + @api.onchange('journal_id') + def _onchange_journal_id(self): + if hasattr(super(HrExpenseRegisterPaymentWizard, self), '_onchange_journal_id'): + super(HrExpenseRegisterPaymentWizard, self)._onchange_journal_id() + if self.journal_id.check_manual_sequencing: + self.check_number = self.journal_id.check_sequence_id.number_next_actual + + @api.onchange('amount') + def _onchange_amount(self): + if hasattr(super(HrExpenseRegisterPaymentWizard, self), '_onchange_amount'): + super(HrExpenseRegisterPaymentWizard, self)._onchange_amount() + self.check_amount_in_words = self.env['account.payment']._get_check_amount_in_words(self.amount) + + def get_payment_vals(self): + res = super(HrExpenseRegisterPaymentWizard, self).get_payment_vals() + if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'): + res.update({ + 'check_amount_in_words': self.check_amount_in_words, + 'check_manual_sequencing': self.check_manual_sequencing, + }) + return res diff --git a/addons/hr_expense_check/views/payment.xml b/addons/hr_expense_check/views/payment.xml new file mode 100644 index 00000000000..8f1ce4981a6 --- /dev/null +++ b/addons/hr_expense_check/views/payment.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<data> + <record id="hr_expense_register_payment_view_form_check_inherit" model="ir.ui.view"> + <field name="name">hr.expense.register.payment.wizard.form.check.inherited</field> + <field name="model">hr.expense.register.payment.wizard</field> + <field name="inherit_id" ref="hr_expense.hr_expense_register_payment_view_form" /> + <field name="arch" type="xml"> + <xpath expr="//div[@name='amount_div']" position="after"> + <field name="check_amount_in_words" attrs="{'invisible': [('payment_method_code_2', '!=', 'check_printing')]}" groups="base.group_no_one"/> + </xpath> + <xpath expr="//field[@name='communication']" position="after"> + <field name="payment_method_code_2" invisible="1"/> + <field name="check_manual_sequencing" invisible="1"/> + <field name="check_number" attrs="{'invisible': ['|', ('payment_method_code_2', '!=', 'check_printing'), ('check_manual_sequencing', '=', False)]}"/> + </xpath> + </field> + </record> +</data> diff --git a/addons/hr_holidays/controllers/main.py b/addons/hr_holidays/controllers/main.py index bc517b71476..3b0b5ee1b0d 100644 --- a/addons/hr_holidays/controllers/main.py +++ b/addons/hr_holidays/controllers/main.py @@ -12,7 +12,7 @@ class HrHolidaysController(http.Controller): comparison, record, redirect = MailController._check_token_and_record_or_redirect('hr.holidays', int(res_id), token) if comparison and record: try: - record.action_validate() + record.action_approve() except Exception: return MailController._redirect_to_messaging() return redirect diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py index cc94bd6aec5..4b65828897c 100644 --- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -85,7 +85,7 @@ class HrTimesheetSheet(models.Model): AND id <> %s''', (sheet.date_to, sheet.date_from, new_user_id, sheet.id)) if any(self.env.cr.fetchall()): - raise ValidationError('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.') + raise ValidationError(_('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.')) @api.onchange('employee_id') def onchange_employee_id(self): diff --git a/addons/l10n_be_invoice_bba/i18n/l10n_be_invoice_bba.pot b/addons/l10n_be_invoice_bba/i18n/l10n_be_invoice_bba.pot index 3c6a42e32e4..b3d5a858448 100644 --- a/addons/l10n_be_invoice_bba/i18n/l10n_be_invoice_bba.pot +++ b/addons/l10n_be_invoice_bba/i18n/l10n_be_invoice_bba.pot @@ -44,6 +44,8 @@ msgstr "" #. module: l10n_be_invoice_bba #: constraint:account.invoice:0 +#: code:addons/l10n_be_invoice_bba/models/account_invoice.py:34 +#, python-format msgid "Invalid BBA Structured Communication !" msgstr "" diff --git a/addons/l10n_be_invoice_bba/models/account_invoice.py b/addons/l10n_be_invoice_bba/models/account_invoice.py index b477cde7d10..3cf74d36d97 100644 --- a/addons/l10n_be_invoice_bba/models/account_invoice.py +++ b/addons/l10n_be_invoice_bba/models/account_invoice.py @@ -1,184 +1,184 @@ -# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-# Copyright (c) 2011 Noviat nv/sa (www.noviat.be). All rights reserved.
-
-import random
-import re
-
-from odoo import api, fields, models, _
-from odoo.exceptions import UserError, ValidationError
-
-"""
-account.invoice object:
- - Add support for Belgian structured communication
- - Rename 'reference' field labels to 'Communication'
-"""
-
-
-class AccountInvoice(models.Model):
- _inherit = 'account.invoice'
-
- @api.model
- def _get_reference_type(self):
- """Add BBA Structured Communication Type and change labels from 'reference' into 'communication' """
- res = super(AccountInvoice, self)._get_reference_type()
- res[[i for i, x in enumerate(res) if x[0] == 'none'][0]] = ('none', _('Free Communication'))
- res.append(('bba', 'BBA Structured Communication'))
- return res
-
- reference_type = fields.Selection('_get_reference_type', string='Payment Reference',
- required=True, readonly=True)
-
- @api.constrains('reference', 'reference_type')
- def _check_communication(self):
- for inv in self:
- if inv.reference_type == 'bba' and not self.check_bbacomm(inv.reference):
- raise ValidationError('Invalid BBA Structured Communication !')
-
- def check_bbacomm(self, val):
- supported_chars = '0-9+*/ '
- pattern = re.compile('[^' + supported_chars + ']')
- if pattern.findall(val or ''):
- return False
- bbacomm = re.sub('\D', '', val or '')
- if len(bbacomm) == 12:
- base = int(bbacomm[:10])
- mod = base % 97 or 97
- if mod == int(bbacomm[-2:]):
- return True
-
- @api.onchange('partner_id', 'type', 'reference_type')
- def _onchange_partner_id(self):
- result = super(AccountInvoice, self)._onchange_partner_id()
- reference = False
- reference_type = 'none'
- if self.partner_id:
- if (self.type == 'out_invoice'):
- reference_type = self.partner_id.out_inv_comm_type
- if reference_type:
- reference = self.generate_bbacomm(self.type, reference_type, self.partner_id.id, '')['value']['reference']
- self.reference_type = reference_type or 'none'
- self.reference = reference
- return result
-
- def generate_bbacomm(self, type, reference_type, partner_id, reference):
- reference = reference or ''
- algorithm = False
- if partner_id:
- algorithm = self.env['res.partner'].browse(partner_id).out_inv_comm_algorithm
- algorithm = algorithm or 'random'
- if (type == 'out_invoice'):
- if reference_type == 'bba':
- if algorithm == 'date':
- if not self.check_bbacomm(reference):
- date = fields.Date.from_string(fields.Date.today())
- doy = date.strftime('%j')
- year = date.strftime('%Y')
- seq = '001'
- invoices = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'),
- ('reference', 'like', '+++%s/%s/%%' % (doy, year))], order='reference')
- if invoices:
- prev_seq = int(invoices[-1].reference[12:15])
- if prev_seq < 999:
- seq = '%03d' % (prev_seq + 1)
- else:
- raise UserError(_('The daily maximum of outgoing invoices with an automatically generated BBA Structured Communications has been exceeded!'
- '\nPlease create manually a unique BBA Structured Communication.'))
- bbacomm = doy + year + seq
- base = int(bbacomm)
- mod = base % 97 or 97
- reference = '+++%s/%s/%s%02d+++' % (doy, year, seq, mod)
- elif algorithm == 'partner_ref':
- if not self.check_bbacomm(reference):
- partner_ref = self.env['res.partner'].browse(partner_id).ref
- partner_ref_nr = re.sub('\D', '', partner_ref or '')
- if (len(partner_ref_nr) < 3) or (len(partner_ref_nr) > 7):
- raise UserError(_('The Partner should have a 3-7 digit Reference Number for the generation of BBA Structured Communications!'
- '\nPlease correct the Partner record.'))
- else:
- partner_ref_nr = partner_ref_nr.ljust(7, '0')
- seq = '001'
- invoices = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'),
- ('reference', 'like', '+++%s/%s/%%' % (partner_ref_nr[:3], partner_ref_nr[3:]))], order='reference')
- if invoices:
- prev_seq = int(invoices[-1].reference[12:15])
- if prev_seq < 999:
- seq = '%03d' % (prev_seq + 1)
- else:
- raise UserError(_('The daily maximum of outgoing invoices with an automatically generated BBA Structured Communications has been exceeded!'
- '\nPlease create manually a unique BBA Structured Communication.'))
- bbacomm = partner_ref_nr + seq
- base = int(bbacomm)
- mod = base % 97 or 97
- reference = '+++%s/%s/%s%02d+++' % (partner_ref_nr[:3], partner_ref_nr[3:], seq, mod)
- elif algorithm == 'random':
- if not self.check_bbacomm(reference):
- base = random.randint(1, 9999999999)
- bbacomm = str(base).rjust(10, '0')
- base = int(bbacomm)
- mod = base % 97 or 97
- mod = str(mod).rjust(2, '0')
- reference = '+++%s/%s/%s%s+++' % (bbacomm[:3], bbacomm[3:7], bbacomm[7:], mod)
- else:
- raise UserError(_("Unsupported Structured Communication Type Algorithm '%s' !"
- "\nPlease contact your Odoo support channel.") % algorithm)
- return {'value': {'reference': reference}}
-
- @api.model
- def create(self, vals):
- reference = vals.get('reference', False)
- reference_type = vals.get('reference_type', False)
- if vals.get('type') == 'out_invoice' and not reference_type:
- # fallback on default communication type for partner
- partner = self.env['res.partner'].browse(vals['partner_id'])
- reference_type = partner.out_inv_comm_type
- if reference_type == 'bba':
- reference = self.generate_bbacomm(vals['type'], reference_type, partner.id, '')['value']['reference']
- vals.update({
- 'reference_type': reference_type or 'none',
- 'reference': reference,
- })
-
- if reference_type == 'bba':
- if not reference:
- raise UserError(_('Empty BBA Structured Communication!'
- '\nPlease fill in a unique BBA Structured Communication.'))
- if self.check_bbacomm(reference):
- reference = re.sub('\D', '', reference)
- vals['reference'] = '+++' + reference[0:3] + '/' + reference[3:7] + '/' + reference[7:] + '+++'
- same_ids = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'), ('reference', '=', vals['reference'])])
- if same_ids:
- raise UserError(_('The BBA Structured Communication has already been used!'
- '\nPlease create manually a unique BBA Structured Communication.'))
- return super(AccountInvoice, self).create(vals)
-
- @api.multi
- def write(self, vals):
- for invoice in self:
- if 'reference_type' in vals:
- reference_type = vals['reference_type']
- else:
- reference_type = invoice.reference_type or ''
-
- if reference_type == 'bba' and 'reference' in vals:
- if self.check_bbacomm(vals['reference']):
- reference = re.sub('\D', '', vals['reference'])
- vals['reference'] = '+++' + reference[0:3] + '/' + reference[3:7] + '/' + reference[7:] + '+++'
- same_ids = self.search([('id', '!=', invoice.id), ('type', '=', 'out_invoice'),
- ('reference_type', '=', 'bba'), ('reference', '=', vals['reference'])])
- if same_ids:
- raise UserError(_('The BBA Structured Communication has already been used!'
- '\nPlease create manually a unique BBA Structured Communication.'))
- return super(AccountInvoice, self).write(vals)
-
- @api.multi
- def copy(self, default=None):
- self.ensure_one()
- default = default or {}
- if self.type in ['out_invoice']:
- reference_type = self.reference_type or 'none'
- default['reference_type'] = reference_type
- if reference_type == 'bba':
- default['reference'] = self.generate_bbacomm(self.type, reference_type, self.partner_id.id, '')['value']['reference']
- return super(AccountInvoice, self).copy(default)
+# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (c) 2011 Noviat nv/sa (www.noviat.be). All rights reserved. + +import random +import re + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError + +""" +account.invoice object: + - Add support for Belgian structured communication + - Rename 'reference' field labels to 'Communication' +""" + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + @api.model + def _get_reference_type(self): + """Add BBA Structured Communication Type and change labels from 'reference' into 'communication' """ + res = super(AccountInvoice, self)._get_reference_type() + res[[i for i, x in enumerate(res) if x[0] == 'none'][0]] = ('none', _('Free Communication')) + res.append(('bba', 'BBA Structured Communication')) + return res + + reference_type = fields.Selection('_get_reference_type', string='Payment Reference', + required=True, readonly=True) + + @api.constrains('reference', 'reference_type') + def _check_communication(self): + for inv in self: + if inv.reference_type == 'bba' and not self.check_bbacomm(inv.reference): + raise ValidationError(_('Invalid BBA Structured Communication !')) + + def check_bbacomm(self, val): + supported_chars = '0-9+*/ ' + pattern = re.compile('[^' + supported_chars + ']') + if pattern.findall(val or ''): + return False + bbacomm = re.sub('\D', '', val or '') + if len(bbacomm) == 12: + base = int(bbacomm[:10]) + mod = base % 97 or 97 + if mod == int(bbacomm[-2:]): + return True + + @api.onchange('partner_id', 'type', 'reference_type') + def _onchange_partner_id(self): + result = super(AccountInvoice, self)._onchange_partner_id() + reference = False + reference_type = 'none' + if self.partner_id: + if (self.type == 'out_invoice'): + reference_type = self.partner_id.out_inv_comm_type + if reference_type: + reference = self.generate_bbacomm(self.type, reference_type, self.partner_id.id, '')['value']['reference'] + self.reference_type = reference_type or 'none' + self.reference = reference + return result + + def generate_bbacomm(self, type, reference_type, partner_id, reference): + reference = reference or '' + algorithm = False + if partner_id: + algorithm = self.env['res.partner'].browse(partner_id).out_inv_comm_algorithm + algorithm = algorithm or 'random' + if (type == 'out_invoice'): + if reference_type == 'bba': + if algorithm == 'date': + if not self.check_bbacomm(reference): + date = fields.Date.from_string(fields.Date.today()) + doy = date.strftime('%j') + year = date.strftime('%Y') + seq = '001' + invoices = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'), + ('reference', 'like', '+++%s/%s/%%' % (doy, year))], order='reference') + if invoices: + prev_seq = int(invoices[-1].reference[12:15]) + if prev_seq < 999: + seq = '%03d' % (prev_seq + 1) + else: + raise UserError(_('The daily maximum of outgoing invoices with an automatically generated BBA Structured Communications has been exceeded!' + '\nPlease create manually a unique BBA Structured Communication.')) + bbacomm = doy + year + seq + base = int(bbacomm) + mod = base % 97 or 97 + reference = '+++%s/%s/%s%02d+++' % (doy, year, seq, mod) + elif algorithm == 'partner_ref': + if not self.check_bbacomm(reference): + partner_ref = self.env['res.partner'].browse(partner_id).ref + partner_ref_nr = re.sub('\D', '', partner_ref or '') + if (len(partner_ref_nr) < 3) or (len(partner_ref_nr) > 7): + raise UserError(_('The Partner should have a 3-7 digit Reference Number for the generation of BBA Structured Communications!' + '\nPlease correct the Partner record.')) + else: + partner_ref_nr = partner_ref_nr.ljust(7, '0') + seq = '001' + invoices = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'), + ('reference', 'like', '+++%s/%s/%%' % (partner_ref_nr[:3], partner_ref_nr[3:]))], order='reference') + if invoices: + prev_seq = int(invoices[-1].reference[12:15]) + if prev_seq < 999: + seq = '%03d' % (prev_seq + 1) + else: + raise UserError(_('The daily maximum of outgoing invoices with an automatically generated BBA Structured Communications has been exceeded!' + '\nPlease create manually a unique BBA Structured Communication.')) + bbacomm = partner_ref_nr + seq + base = int(bbacomm) + mod = base % 97 or 97 + reference = '+++%s/%s/%s%02d+++' % (partner_ref_nr[:3], partner_ref_nr[3:], seq, mod) + elif algorithm == 'random': + if not self.check_bbacomm(reference): + base = random.randint(1, 9999999999) + bbacomm = str(base).rjust(10, '0') + base = int(bbacomm) + mod = base % 97 or 97 + mod = str(mod).rjust(2, '0') + reference = '+++%s/%s/%s%s+++' % (bbacomm[:3], bbacomm[3:7], bbacomm[7:], mod) + else: + raise UserError(_("Unsupported Structured Communication Type Algorithm '%s' !" + "\nPlease contact your Odoo support channel.") % algorithm) + return {'value': {'reference': reference}} + + @api.model + def create(self, vals): + reference = vals.get('reference', False) + reference_type = vals.get('reference_type', False) + if vals.get('type') == 'out_invoice' and not reference_type: + # fallback on default communication type for partner + partner = self.env['res.partner'].browse(vals['partner_id']) + reference_type = partner.out_inv_comm_type + if reference_type == 'bba': + reference = self.generate_bbacomm(vals['type'], reference_type, partner.id, '')['value']['reference'] + vals.update({ + 'reference_type': reference_type or 'none', + 'reference': reference, + }) + + if reference_type == 'bba': + if not reference: + raise UserError(_('Empty BBA Structured Communication!' + '\nPlease fill in a unique BBA Structured Communication.')) + if self.check_bbacomm(reference): + reference = re.sub('\D', '', reference) + vals['reference'] = '+++' + reference[0:3] + '/' + reference[3:7] + '/' + reference[7:] + '+++' + same_ids = self.search([('type', '=', 'out_invoice'), ('reference_type', '=', 'bba'), ('reference', '=', vals['reference'])]) + if same_ids: + raise UserError(_('The BBA Structured Communication has already been used!' + '\nPlease create manually a unique BBA Structured Communication.')) + return super(AccountInvoice, self).create(vals) + + @api.multi + def write(self, vals): + for invoice in self: + if 'reference_type' in vals: + reference_type = vals['reference_type'] + else: + reference_type = invoice.reference_type or '' + + if reference_type == 'bba' and 'reference' in vals: + if self.check_bbacomm(vals['reference']): + reference = re.sub('\D', '', vals['reference']) + vals['reference'] = '+++' + reference[0:3] + '/' + reference[3:7] + '/' + reference[7:] + '+++' + same_ids = self.search([('id', '!=', invoice.id), ('type', '=', 'out_invoice'), + ('reference_type', '=', 'bba'), ('reference', '=', vals['reference'])]) + if same_ids: + raise UserError(_('The BBA Structured Communication has already been used!' + '\nPlease create manually a unique BBA Structured Communication.')) + return super(AccountInvoice, self).write(vals) + + @api.multi + def copy(self, default=None): + self.ensure_one() + default = default or {} + if self.type in ['out_invoice']: + reference_type = self.reference_type or 'none' + default['reference_type'] = reference_type + if reference_type == 'bba': + default['reference'] = self.generate_bbacomm(self.type, reference_type, self.partner_id.id, '')['value']['reference'] + return super(AccountInvoice, self).copy(default) diff --git a/addons/l10n_cn/__manifest__.py b/addons/l10n_cn/__manifest__.py index 02b2fcb1551..80c479ba517 100644 --- a/addons/l10n_cn/__manifest__.py +++ b/addons/l10n_cn/__manifest__.py @@ -27,5 +27,4 @@ 'data/res_country_state_data.xml', 'data/account_account_type_data.xml', ], - 'license': 'GPL-3', } diff --git a/addons/l10n_cn_small_business/__manifest__.py b/addons/l10n_cn_small_business/__manifest__.py index 1e45d2de892..c5e504f2da7 100644 --- a/addons/l10n_cn_small_business/__manifest__.py +++ b/addons/l10n_cn_small_business/__manifest__.py @@ -27,5 +27,4 @@ 'data/l10n_cn_small_business_chart_data.xml', 'data/account_chart_template_data.yml', ], - 'license': 'GPL-3', } diff --git a/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml b/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml index 1bb56ba6c77..370ef50f5dc 100644 --- a/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml +++ b/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml @@ -550,7 +550,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <!-- Account Tax Tags--> <record id="tax_tag1" model="account.account.tag"> - <field name="name">17%的增值税销项税</field> + <field name="name">增值税17%销项税</field> </record> <record id="tax_tag2" model="account.account.tag"> <field name="name">增值税17%进项税</field> @@ -559,8 +559,8 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <!-- 增值税 --> <record id="vats_small_business" model="account.tax.template"> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> - <field name="name">17%的增值税销项税 - 中国小企业会计科目表</field> - <field name="description">17%的增值税销项税</field> + <field name="name">增值税17%销项税 - 中国小企业会计科目表</field> + <field name="description">增值税17%销项税</field> <field eval="17" name="amount"/> <field name="amount_type">percent</field> <field name="refund_account_id" ref="small_business_chart2221"/> diff --git a/addons/l10n_cn_standard/__manifest__.py b/addons/l10n_cn_standard/__manifest__.py index e29a1a4ecc1..d770d4eaa0f 100644 --- a/addons/l10n_cn_standard/__manifest__.py +++ b/addons/l10n_cn_standard/__manifest__.py @@ -36,5 +36,4 @@ Including the following data in the Accounting Standards for Business Enterprise 'data/account_chart_template_data.xml', 'data/account_chart_template_data.yml', ], - 'license': 'GPL-3', } diff --git a/addons/l10n_do/__manifest__.py b/addons/l10n_do/__manifest__.py index cf4af681219..9aac8992d2e 100644 --- a/addons/l10n_do/__manifest__.py +++ b/addons/l10n_do/__manifest__.py @@ -90,9 +90,9 @@ en Odoo): 'data': [ # Basic accounting data 'data/l10n_do_chart_data.xml', + 'data/account_account_tag_data.xml', 'data/account.account.template.csv', 'data/account_chart_template_data.xml', - 'data/account_account_tag_data.xml', 'data/account.tax.template.xml', # Country States 'data/l10n_do_state_data.xml', diff --git a/addons/l10n_do/data/account.account.template.csv b/addons/l10n_do/data/account.account.template.csv index 2879acc2e86..c24b83d6e1d 100644 --- a/addons/l10n_do/data/account.account.template.csv +++ b/addons/l10n_do/data/account.account.template.csv @@ -1,254 +1,271 @@ -id,code,name,user_type_id/id,reconcile,chart_template_id/id -do_niif_11010301,11010301,Depósitos a Corto Plazo,account.data_account_type_liquidity,FALSE,do_chart_template -do_niif_11020100,11020100,Bonos y Acciones Temporales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11020200,11020200,Operaciones en Bolsa,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11020300,11020300,Otros Valores Negociables,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11030101,11030101,Cheques Devueltos por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030102,11030102,Intereses por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030201,11030201,Cuenta por Cobrar a Clientes,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030202,11030202,Cuenta por Cobrar a Funcionarios y Empleados,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030203,11030203,Cuenta por Cobrar a Afiliadas,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030204,11030204,Cuenta por Cobrar a Accionistas,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11030205,11030205,Otras Cuentas por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template -do_niif_11040100,11040100,Provisión Incobrables a Clientes,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11040200,11040200,Provisión Incobrables a Funcionarios y Empleados,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11040300,11040300,Provisión Incobrables a Afiliadas,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11040400,11040400,Provisión Incobrables a Accionistas,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11040500,11040500,Provisión Otras Cuentas Incobrables,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11050100,11050100,Inventario de Mercancías o Productos Terminados,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11050200,11050200,Inventario de Materia Prima,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11050300,11050300,Inventario en Tránsito,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11050400,11050400,Materiales y Suministros en Inventario,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11050500,11050500,Combustibles en Inventario,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11060100,11060100,Deterioro Acum. de Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11060200,11060200,Deterioro Acum. de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11060300,11060300,Deterioro Acum. de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11070100,11070100,Obsolescencia Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11070200,11070200,Obsolescencia Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11070300,11070300,Obsolescencia Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080101,11080101,ITBIS Pagado en Compras Locales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080102,11080102,ITBIS Pagado en Servicios Locales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080103,11080103,ITBIS Pagado por Importaciones,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080301,11080301,ITBIS Pagado en Ventas con Tarjeta de Crédito (Norma 08-04),account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080302,11080302,Saldo a Favor ITBIS,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11080303,11080303,Saldo a Favor ISR,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11090100,11090100,Acciones Temporales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11090200,11090200,Depósitos a Plazo Temporales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11090300,11090300,Bonos Temporales,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_11100100,11100100,Gastos Pagados por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template -do_niif_11100200,11100200,Renta Pagada por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template -do_niif_11100300,11100300,Seguros Pagados por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template -do_niif_11100400,11100400,Anticipos ISR,account.data_account_type_prepayments,FALSE,do_chart_template -do_niif_11100500,11100500,Anticipos Gastos,account.data_account_type_prepayments,FALSE,do_chart_template -do_niif_12010101,12010101,Terrenos,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010102,12010102,Edificaciones,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010201,12010201,Equipo de Transporte,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010202,12010202,Mobiliario y Equipos,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010301,12010301,Maquinaria,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010302,12010302,Herramientas,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010303,12010303,Instalaciones,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12010304,12010304,Misceláneos u otros activos,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12020100,12020100,Depreciación Acum. de Edificios,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12020200,12020200,Depreciación Acum. de Equipo de Transporte,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12020300,12020300,Depreciación Acum. de Mobiliario y Equipos,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12020400,12020400,Depreciación Acum. de Maquinaria,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12020500,12020500,Depreciación Acum. de Herramientas,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12020600,12020600,Depreciación Acum. de Instalaciones,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12040100,12040100,Marcas y Patentes,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12040200,12040200,Programas Informáticos,account.data_account_type_fixed_assets,FALSE,do_chart_template -do_niif_12050100,12050100,Edificios y Locales Arrendados,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_12050200,12050200,Maquinaria y Equipo Arrendados,account.data_account_type_current_assets,FALSE,do_chart_template -do_niif_12050300,12050300,Instalaciones Arrendadas,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12060100,12060100,Depreciación Acum. Edificios y Locales,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12060200,12060200,Depreciación Acum. Maquinaria y Equipo,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12060300,12060300,Depreciación Acum. Instalaciones,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12070100,12070100,Deterioro Valor Acum. Edificios y Locales,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12070200,12070200,Deterioro Valor Acum. Maquinaria y Equipo,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12070300,12070300,Deterioro Valor Acum. Instalaciones,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12080100,12080100,Acciones en otras sociedades,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12090100,12090100,Diferencias temporales deducibles,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12090200,12090200,Gastos Organizacionales Diferidos,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_12090300,12090300,ISR Deducible,account.data_account_type_non_current_assets,FALSE,do_chart_template -do_niif_21010100,21010100,Préstamos Bancarios por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21010200,21010200,Cuenta por Pagar Proveedores Locales,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21010300,21010300,Cuenta por Pagar Proveedores del Exterior,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21010400,21010400,Nóminas por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21010500,21010500,Intereses por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21010600,21010600,Otras Cuentas por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21020100,21020100,Dividendos por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21020200,21020200,Comisiones por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21020300,21020300,Bonificaciones por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21020400,21020400,Regalía Pascual por Pagar,account.data_account_type_payable,TRUE,do_chart_template -do_niif_21030101,21030101,ITBIS por Venta Servicios,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030102,21030102,ITBIS por Venta Bienes,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030103,21030103,ITBIS por Venta Bienes - al Exterior,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030201,21030201,ITBIS Retenido Persona Jurídica (N 02-05),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030202,21030202,ITBIS Retenido Persona Física (R 293-11),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030203,21030203,ITBIS Retenido a Entidades No Lucrativas (N 01-2011),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030204,21030204,ITBIS Retenido por Servicios Profesionales Liberales (N 02-05),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030205,21030205,ITBIS Retenido a Proveedores Informales de Bienes (N 08-10),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030301,21030301,ISR Retenido por Honorarios Profesionales de Personas Físicas,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030302,21030302,ISR Retenido por Alquileres Pagados a Personas Físicas,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030303,21030303,ISR Retenido por Dividendos Pagados,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030304,21030304,ISR Retenido por Intereses Pagados al Exterior,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030305,21030305,ISR Retenido por Intereses Pagados,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030306,21030306,ISR Retenido por Transferencias de Títulos y Propiedades,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030307,21030307,ISR Retenido por Remesas al Exterior (Ley 253-12),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030308,21030308,Otras Retenciones (N 07-07),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030309,21030309,Otras Retenciones,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030401,21030401,Retención de Seguro Familiar de Salud (SFS),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030402,21030402,"Seguro de vejez, Discapacidad y Sobre Vivencia (AFP)",account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030404,21030404,Impuesto al Salario (ISR),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030501,21030501,ISR por Pagar,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030502,21030502,Anticipos ISR por Pagar,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030503,21030503,Seguro de Riesgo Laboral (SRL),account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030504,21030504,INFOTEP,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21030505,21030505,Otros Impuestos por Pagar,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21040100,21040100,Provisión Pago Alquileres,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21040200,21040200,Provisión Arrendamiento Financiero,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21040300,21040300,Provisión Gastos Fijos,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21050100,21050100,Tarjeta de Crédito Empresarial,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_21050200,21050200,Prima de Tarjetas de Crédito,account.data_account_type_current_liabilities,FALSE,do_chart_template -do_niif_22010100,22010100,Préstamos Bancarios Largo Plazo,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22010200,22010200,Préstamos Hipotecarios LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22012300,22012300,Otros Préstamos LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22020100,22020100,Provisión Beneficios de Empleados LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22020200,22020200,Provisión Prestaciones Laborales,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22020300,22020300,Provisión Indemnizaciones,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22030100,22030100,Anticipos de Clientes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22030200,22030200,Garantías de Clientes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_22040100,22040100,Provisión Arrendamiento Financiero LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template -do_niif_31010100,31010100,Capital Social Autorizado,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31010200,31010200,Capital Social en Acciones no Emitidas,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31010300,31010300,Capital Social Pagado,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31010400,31010400,Capital Social No Pagado,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31010500,31010500,Acciones o Participaciones en Tesorería,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31020100,31020100,Superávit por Revaluación de Terrenos,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31020200,31020200,Superávit por Revaluación de Edificaciones,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31020300,31020300,Superávit por Revaluación de Instalaciones,account.data_account_type_equity,FALSE,do_chart_template -do_niif_31020400,31020400,Superávit por Revaluación de Mobiliario y Equipo,account.data_account_type_equity,FALSE,do_chart_template -do_niif_32010100,32010100,Reserva Legal,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33010100,33010100,Utilidades de Ejercicios Anteriores,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33010200,33010200,Pérdidas de Ejercicios Anteriores,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33020100,33020100,Utilidad del Ejercicio,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33020200,33020200,Pérdidas del Ejercicio,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33040100,33040100,Superávit en Reserva,account.data_account_type_equity,FALSE,do_chart_template -do_niif_33040200,33040200,Déficit en Reserva,account.data_account_type_equity,FALSE,do_chart_template -do_niif_41010100,41010100,Ventas Bienes,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41010200,41010200,Ventas Bienes - Al Exterior,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41020100,41020100,Ventas Servicios,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41020200,41020200,Ventas Servicios - Al Exterior,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41030100,41030100,Devoluciones de Bienes,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41030200,41030200,Devoluciones por Servicios,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_41040100,41040100,Descuentos por Ventas (por NC),account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42010100,42010100,Intereses sobre Certificados,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42010200,42010200,Intereses en Cuentas Bancarias,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42010300,42010300,Intereses por Financiamientos,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42020100,42020100,Ingresos por Ventas de Activos,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42030100,42030100,Ingresos por Dividendos Ganados,account.data_account_type_revenue,FALSE,do_chart_template -do_niif_42040100,42040100,Ingresos por Diferencia Cambiaria,account.data_account_type_other_income,FALSE,do_chart_template -do_niif_42040200,42040200,Cobro de Cuentas Incobrables,account.data_account_type_other_income,FALSE,do_chart_template -do_niif_42040300,42040300,Otros Ingresos,account.data_account_type_other_income,FALSE,do_chart_template -do_niif_51010100,51010100,Costos de Bienes,account.data_account_type_direct_costs,FALSE,do_chart_template -do_niif_51010200,51010200,Otros Costos de Venta,account.data_account_type_direct_costs,FALSE,do_chart_template -do_niif_51020100,51020100,Materia Prima,account.data_account_type_direct_costs,FALSE,do_chart_template -do_niif_51020200,51020200,Mano de Obra,account.data_account_type_direct_costs,FALSE,do_chart_template -do_niif_51020300,51020300,Costos Indirectos,account.data_account_type_direct_costs,FALSE,do_chart_template -do_niif_52010100,52010100,Sueldos y Salarios,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010200,52010200,Regalía Pascual,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010300,52010300,Retribuciones Complementarias,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010400,52010400,Bono Vacacional,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010500,52010500,Bono por Desempeño,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010600,52010600,Comisiones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010700,52010700,Indemnizaciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010800,52010800,Bonificaciones y Gratificaciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010900,52010900,Dietas al Personal,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52011000,52011000,Cursos y Entrenamientos,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52011100,52011100,Prestaciones Laborales (Preaviso y Cesantia),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52011200,52011200,Incentivos,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52011300,52011300,Horas Extras,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52011400,52011400,Otros Gastos de Personal,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010102,52010102,Aportes a la Administradora de Fondos de Pensiones (AFP),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010103,52010103,Aportes al Seguro Riesgo Laboral (SRL),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010104,52010104,Seguro Familiar de Salud (SFS),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010201,52010201,Aportes a la Administradora de Riesgos de Salud (ARS),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010202,52010202,Seguros al Personal,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010203,52010203,Planes Complementarios de Salud,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010204,52010204,Seguros de Vida,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52010205,52010205,Aportes al Infotep,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020100,52020100,Energía Eléctrica,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020200,52020200,Comunicaciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020300,52020300,Papelería y útiles,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020400,52020400,Útiles de Aseo y Limpieza,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020500,52020500,Material de Empaque,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020600,52020600,Seguro de Edificio,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020700,52020700,Combustibles y Lubricantes,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020800,52020800,Alquileres,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52020900,52020900,Franquicias,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021000,52021000,Inducción a Vendedores,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021100,52021100,Seguros,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021200,52021200,Servicios de Mensajería,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021300,52021300,Flete y Carga,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021400,52021400,Cuotas y Suscripciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52021500,52021500,Otros Gastos de Administración,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030101,52030101,Legales (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030102,52030102,Contabilidad y Auditoría (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030103,52030103,Tecnología (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030104,52030104,Mantenimiento de Planta (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030105,52030105,Mantenimiento del Local (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030106,52030106,Mantenimiento Mobiliario y Equipos (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030107,52030107,Asesorías (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030108,52030108,Fumigaciones (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030109,52030109,Copias y Escaneos (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030110,52030110,Servicios de Vigilancia (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030111,52030111,Otros Servicios Profesionales (P. Física),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030201,52030201,Legales (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030202,52030202,Contabilidad y Auditoría (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030203,52030203,Tecnología (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030204,52030204,Mantenimiento de Planta (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030205,52030205,Mantenimiento del Local (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030206,52030206,Mantenimiento Mobiliario y Equipos (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030207,52030207,Asesorías (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030208,52030208,Fumigaciones (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030209,52030209,Copias y Escaneos (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030210,52030210,Servicios de Vigilancia (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030211,52030211,Otros Servicios Profesionales (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030301,52030301,Honorarios por Servicios del Exterior - Relacionadas,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52030302,52030302,Honorarios por Servicios del Exterior - Terceros,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52040100,52040100,Gastos por Depreciación de Edificios,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52040200,52040200,Gastos por Depreciación de Equipo de Transporte,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52040300,52040300,Gastos por Depreciación de Mobiliario y Equipos,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52040400,52040400,Gastos por Depreciación de Maquinaria,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52040500,52040500,Gastos por Depreciación de Herramientas,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52040600,52040600,Gastos por Depreciación de Instalaciones,account.data_account_type_depreciation,FALSE,do_chart_template -do_niif_52050100,52050100,Gastos por Reparación de Edificios,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52050200,52050200,Gastos por Reparación de Equipo de Transporte,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52050300,52050300,Gastos por Reparación de Mobiliario y Equipos,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52050400,52050400,Gastos por Reparación de Maquinaria,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52050500,52050500,Gastos por Reparación de Herramientas,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52050600,52050600,Gastos por Reparación de Instalaciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060100,52060100,Relaciones Públicas,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060200,52060200,Publicidad,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060300,52060300,Viajes,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060400,52060400,Donaciones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060500,52060500,Donaciones a ProIndustria (Ley 392-07),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060600,52060600,Promociones,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52060700,52060700,Otros Gastos de Representación,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070100,52070100,Gastos por Préstamos Bancarios,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070200,52070200,Retención por Cheques y Transacciones Electrónicas (0.015%),account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070300,52070300,Intereses Bancarios,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070400,52070400,Comisiones Bancarias,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070500,52070500,Comisión de Tarjeta de Crédito,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070600,52070600,Nota de Cargos por Banco,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070700,52070700,Seguro sobre Préstamos Bancarios,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070800,52070800,Diferencia Negativa Cambiaria,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52070900,52070900,Otro Gastos Financieros,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080100,52080100,Gastos por Siniestros,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080200,52080200,Pérdidas por Ventas de Activos Fijos,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080300,52080300,Pérdidas por Cuentas Incobrables,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080400,52080400,Gastos por ISR,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080500,52080500,Impuestos a los Activos,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080600,52080600,Penalidades/Recargos de DGII,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080700,52080700,Penalidades/Recargos de TSS,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_52080800,52080800,Gastos Sin Comprobante,account.data_account_type_expenses,FALSE,do_chart_template -do_niif_61010100,61010100,Pérdidas y Ganancias,account.data_unaffected_earnings,FALSE,do_chart_template +id,code,name,user_type_id/id,reconcile,chart_template_id/id,tag_ids/id +do_niif_11010301,11010301,Depósitos a corto plazo en RD$,account.data_account_type_liquidity,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1101,account_tag_110103" +do_niif_11010302,11010302,Depósitos a corto plazo en USD$,account.data_account_type_liquidity,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1101,account_tag_110103" +do_niif_11020100,11020100,Bonos y Acciones Temporales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1102" +do_niif_11020200,11020200,Operaciones en Bolsa,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1102" +do_niif_11020300,11020300,Otros Valores Negociables,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1102" +do_niif_11030101,11030101,Cheques Devueltos por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110301" +do_niif_11030102,11030102,Intereses por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110301" +do_niif_11030201,11030201,Cuenta por Cobrar a Clientes,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110302" +do_niif_11030202,11030202,Cuenta por Cobrar a Funcionarios y Empleados,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110302" +do_niif_11030203,11030203,Cuenta por Cobrar a Afiliadas,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110302" +do_niif_11030204,11030204,Cuenta por Cobrar a Accionistas,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110302" +do_niif_11030205,11030205,Otras Cuentas por Cobrar,account.data_account_type_receivable,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1103,account_tag_110302" +do_niif_11040100,11040100,Provisión Incobrables a Clientes,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1104" +do_niif_11040200,11040200,Provisión Incobrables a Funcionarios y Empleados,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1104" +do_niif_11040300,11040300,Provisión Incobrables a Afiliadas,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1104" +do_niif_11040400,11040400,Provisión Incobrables a Accionistas,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1104" +do_niif_11040500,11040500,Provisión Otras Cuentas Incobrables,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1104" +do_niif_11050100,11050100,Inventario de Mercancías o Productos Terminados,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11050200,11050200,Inventario de Materia Prima,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11050300,11050300,Inventario en Tránsito,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11050400,11050400,Materiales y Suministros en Inventario,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11050500,11050500,Combustibles en Inventario,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11050600,11050600,Bienes Enviados No Facturados,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105" +do_niif_11060100,11060100,Deterioro Acum. de Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106" +do_niif_11060200,11060200,Deterioro Acum. de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106" +do_niif_11060300,11060300,Deterioro Acum. de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106" +do_niif_11070100,11070100,Obsolescencia Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1107" +do_niif_11070200,11070200,Obsolescencia Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1107" +do_niif_11070300,11070300,Obsolescencia Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1107" +do_niif_11080101,11080101,ITBIS Pagado en Compras Locales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110801" +do_niif_11080102,11080102,ITBIS Pagado en Servicios Locales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110801" +do_niif_11080103,11080103,ITBIS Pagado por Importaciones,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110801" +do_niif_11080301,11080301,ITBIS Pagado en Ventas con Tarjeta de Crédito (Norma 08-04),account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110803" +do_niif_11080302,11080302,Saldo a Favor ITBIS,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110803" +do_niif_11080303,11080303,Saldo a Favor ISR,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1108,account_tag_110803" +do_niif_11090100,11090100,Acciones Temporales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1109" +do_niif_11090200,11090200,Depósitos a Plazo Temporales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1109" +do_niif_11090300,11090300,Bonos Temporales,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1109" +do_niif_11090400,11090400,Fianzas y Depósitos,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1109" +do_niif_11100100,11100100,Gastos Pagados por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_11100200,11100200,Renta Pagada por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_11100300,11100300,Seguros Pagados por Anticipado,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_11100400,11100400,Anticipos ISR,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_11100500,11100500,Anticipos Gastos,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_11100600,11100600,Anticipos 1% Activos,account.data_account_type_prepayments,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1110" +do_niif_12010101,12010101,Terrenos,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120101" +do_niif_12010102,12010102,Edificaciones,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120101" +do_niif_12010103,12010103,Mejoras en Edificaciones,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120101" +do_niif_12010201,12010201,Equipo de Transporte,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120102" +do_niif_12010202,12010202,Mobiliario y Equipos,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120102" +do_niif_12010301,12010301,Maquinaria,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120103" +do_niif_12010302,12010302,Herramientas,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120103" +do_niif_12010303,12010303,Instalaciones,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120103" +do_niif_12010304,12010304,Misceláneos u otros activos,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1201,account_tag_120103" +do_niif_12020100,12020100,Depreciación Acum. de Edificios,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12020200,12020200,Depreciación Acum. de Equipo de Transporte,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12020300,12020300,Depreciación Acum. de Mobiliario y Equipos,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12020400,12020400,Depreciación Acum. de Maquinaria,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12020500,12020500,Depreciación Acum. de Herramientas,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12020600,12020600,Depreciación Acum. de Instalaciones,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1202" +do_niif_12040100,12040100,Marcas y Patentes,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1204" +do_niif_12040200,12040200,Programas Informáticos,account.data_account_type_fixed_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1204" +do_niif_12050100,12050100,Edificios y Locales Arrendados,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1205" +do_niif_12050200,12050200,Maquinaria y Equipo Arrendados,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1205" +do_niif_12050300,12050300,Instalaciones Arrendadas,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1205" +do_niif_12060100,12060100,Depreciación Acum. Edificios y Locales Arrendados,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1206" +do_niif_12060200,12060200,Depreciación Acum. Maquinaria y Equipo Arrendados,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1206" +do_niif_12060300,12060300,Depreciación Acum. Instalaciones Arrendadas,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1206" +do_niif_12070100,12070100,Deterioro Valor Acum. Edificios y Locales Arrendados,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1207" +do_niif_12070200,12070200,Deterioro Valor Acum. Maquinaria y Equipo Arrendado,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1207" +do_niif_12070300,12070300,Deterioro Valor Acum. Instalaciones Arrendadas,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1207" +do_niif_12080100,12080100,Acciones en otras sociedades,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1208" +do_niif_12090100,12090100,Diferencias temporales deducibles,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1209" +do_niif_12090200,12090200,Gastos Organizacionales Diferidos,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1209" +do_niif_12090300,12090300,ISR Deducible,account.data_account_type_non_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_12,account_tag_1209" +do_niif_21010100,21010100,Préstamos Bancarios por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010200,21010200,Cuenta por Pagar Proveedores Locales,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010300,21010300,Cuenta por Pagar Proveedores del Exterior,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010400,21010400,Cuenta por Pagar a Afiliadas,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010500,21010500,Cuenta por Pagar a Accionistas,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010600,21010600,Nóminas por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010700,21010700,Intereses por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21010800,21010800,Otras Cuentas por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2101" +do_niif_21020100,21020100,Dividendos por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020200,21020200,Comisiones por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020300,21020300,Vacaciones por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020400,21020400,Bonificaciones por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020500,21020500,Regalía Pascual por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020600,21020600,Seguro Médico por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020700,21020700,Seguro Familiar de Salud (SFS) por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020800,21020800,Aporte a Fondo de Pensiones (AFP) por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21020900,21020900,Seguro de Riesgo Laboral (SRL) por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21021000,21021000,INFOTEP por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21021100,21021100,Otras Remuneraciones por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21021200,21021200,Bienes Recibidos no Pagados,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102" +do_niif_21030101,21030101,ITBIS por Venta Servicios,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301" +do_niif_21030102,21030102,ITBIS por Venta Bienes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301" +do_niif_21030103,21030103,ITBIS por Venta Bienes - al Exterior,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301" +do_niif_21030201,21030201,ITBIS Retenido Persona Jurídica (N 02-05),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210302" +do_niif_21030202,21030202,ITBIS Retenido Persona Física (R 293-11),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210302" +do_niif_21030203,21030203,ITBIS Retenido a Entidades No Lucrativas (N 01-2011),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210302" +do_niif_21030204,21030204,ITBIS Retenido por Servicios Profesionales Liberales (N 02-05),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210302" +do_niif_21030205,21030205,ITBIS Retenido a Proveedores Informales de Bienes (N 08-10),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210302" +do_niif_21030301,21030301,ISR Retenido por Honorarios Profesionales de Personas Físicas,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030302,21030302,ISR Retenido por Alquileres Pagados a Personas Físicas,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030303,21030303,ISR Retenido por Dividendos Pagados,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030304,21030304,ISR Retenido por Intereses Pagados al Exterior,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030305,21030305,ISR Retenido por Intereses Pagados,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030306,21030306,ISR Retenido por Transferencias de Títulos y Propiedades,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030307,21030307,ISR Retenido por Remesas al Exterior (Ley 253-12),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030308,21030308,Otras Retenciones (N 07-07),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030309,21030309,Otras Retenciones,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210303" +do_niif_21030401,21030401,Impuesto al Salario (ISR),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210304" +do_niif_21030402,21030402,Retención de Seguro Familiar de Salud (SFS),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210304" +do_niif_21030403,21030403,Retención de Fondo de Pensiones (AFP),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210304" +do_niif_21030404,21030404,Retención de INFOTEP (0.5%),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210304" +do_niif_21030501,21030501,ISR por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210305" +do_niif_21030502,21030502,Anticipos ISR por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210305" +do_niif_21030503,21030503,Propina por Pagar (Ley 16-92),account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210305" +do_niif_21030504,21030504,Otros Impuestos por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210305" +do_niif_21040100,21040100,Provisión Pago Alquileres,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2104" +do_niif_21040200,21040200,Provisión Arrendamiento Financiero,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2104" +do_niif_21040300,21040300,Provisión Gastos Fijos,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2104" +do_niif_21050100,21050100,Tarjeta de Crédito Empresarial,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2105" +do_niif_21050200,21050200,Prima de Tarjetas de Crédito,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2105" +do_niif_22010100,22010100,Préstamos Bancarios Largo Plazo,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2201" +do_niif_22010200,22010200,Préstamos Hipotecarios LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2201" +do_niif_22012300,22012300,Préstamos Accionistas / Particulares,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2201" +do_niif_22020100,22020100,Provisión Beneficios de Empleados LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2202" +do_niif_22020200,22020200,Provisión Prestaciones Laborales,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2202" +do_niif_22020300,22020300,Provisión Indemnizaciones,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2202" +do_niif_22030100,22030100,Anticipos de Clientes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2203" +do_niif_22030200,22030200,Garantías de Clientes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2203" +do_niif_22040100,22040100,Provisión Arrendamiento Financiero LP,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_22,account_tag_2204" +do_niif_31010100,31010100,Capital Social Autorizado,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3101" +do_niif_31010200,31010200,Capital Social en Acciones no Emitidas,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3101" +do_niif_31010300,31010300,Capital Social Pagado,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3101" +do_niif_31010400,31010400,Capital Social No Pagado,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3101" +do_niif_31010500,31010500,Acciones o Participaciones en Tesorería,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3101" +do_niif_31020100,31020100,Superávit por Revaluación de Terrenos,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3102" +do_niif_31020200,31020200,Superávit por Revaluación de Edificaciones,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3102" +do_niif_31020300,31020300,Superávit por Revaluación de Instalaciones,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3102" +do_niif_31020400,31020400,Superávit por Revaluación de Mobiliario y Equipo,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_31,account_tag_3102" +do_niif_32010100,32010100,Reserva Legal,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_32,account_tag_3201" +do_niif_33010100,33010100,Utilidades de Ejercicios Anteriores,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3301" +do_niif_33010200,33010200,Pérdidas de Ejercicios Anteriores,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3301" +do_niif_33020100,33020100,Utilidad del Ejercicio,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3302" +do_niif_33020200,33020200,Pérdidas del Ejercicio,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3302" +do_niif_33040100,33040100,Superávit en Reserva,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3304" +do_niif_33040200,33040200,Déficit en Reserva,account.data_account_type_equity,FALSE,do_chart_template,"account_tag_3,account_tag_33,account_tag_3304" +do_niif_41010100,41010100,Ventas Bienes,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4101" +do_niif_41010200,41010200,Ventas Bienes - Al Exterior,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4101" +do_niif_41020100,41020100,Ventas Servicios,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4102" +do_niif_41020200,41020200,Ventas Servicios - Al Exterior,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4102" +do_niif_41030100,41030100,Devoluciones de Bienes,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4103" +do_niif_41030200,41030200,Devoluciones por Servicios,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4103" +do_niif_41040100,41040100,Descuentos por Ventas (por NC),account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_41,account_tag_4104" +do_niif_42010100,42010100,Intereses sobre Certificados,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4201" +do_niif_42010200,42010200,Intereses en Cuentas Bancarias,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4201" +do_niif_42010300,42010300,Intereses por Financiamientos,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4201" +do_niif_42020100,42020100,Ingresos por Ventas de Activos,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4202" +do_niif_42030100,42030100,Ingresos por Dividendos Ganados,account.data_account_type_revenue,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4203" +do_niif_42040100,42040100,Ingresos por Diferencia Cambiaria,account.data_account_type_other_income,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4204" +do_niif_42040200,42040200,Cobro de Cuentas Incobrables,account.data_account_type_other_income,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4204" +do_niif_42040300,42040300,Otros Ingresos,account.data_account_type_other_income,FALSE,do_chart_template,"account_tag_4,account_tag_42,account_tag_4204" +do_niif_51010100,51010100,Costos de Bienes,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5101" +do_niif_51010200,51010200,Costos de Servicios,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5101" +do_niif_51010300,51010300,Diferencia de Precio,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5101" +do_niif_51020100,51020100,Costos de Materia Prima,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5102" +do_niif_51020200,51020200,Costos de Mano de Obra,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5102" +do_niif_51020300,51020300,Costos Indirectos,account.data_account_type_direct_costs,FALSE,do_chart_template,"account_tag_5,account_tag_51,account_tag_5102" +do_niif_52010100,52010100,Sueldos y Salarios,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010200,52010200,Regalía Pascual,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010300,52010300,Retribuciones Complementarias,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010400,52010400,Bono Vacacional,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010500,52010500,Bono por Desempeño,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010600,52010600,Comisiones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010700,52010700,Indemnizaciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010800,52010800,Bonificaciones y Gratificaciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010900,52010900,Dietas al Personal,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52011000,52011000,Cursos y Entrenamientos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52011100,52011100,Prestaciones Laborales (Preaviso y Cesantia),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52011200,52011200,Incentivos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52011300,52011300,Horas Extras,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52011400,52011400,Otros Gastos de Personal,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201" +do_niif_52010102,52010102,Contribución a la Administradora de Fondos de Pensiones (AFP),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520101" +do_niif_52010103,52010103,Contribución al Seguro Riesgo Laboral (SRL),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520101" +do_niif_52010104,52010104,Contribución al Seguro Familiar de Salud (SFS),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520101" +do_niif_52010201,52010201,Contribución a la Administradora de Riesgos de Salud (ARS),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520102" +do_niif_52010202,52010202,Contribución a Seguros del Personal,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520102" +do_niif_52010203,52010203,Contribución a Planes Complementarios de Salud,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520102" +do_niif_52010204,52010204,Contribución Seguros de Vida,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520102" +do_niif_52010205,52010205,Contribución al INFOTEP,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5201,account_tag_520102" +do_niif_52020100,52020100,Energía Eléctrica,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020200,52020200,Comunicaciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020300,52020300,Papelería y útiles,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020400,52020400,Útiles de Aseo y Limpieza,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020500,52020500,Agua y Basura,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020600,52020600,Seguro de Edificio,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020700,52020700,Combustibles y Lubricantes,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020800,52020800,Alquileres,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52020900,52020900,Franquicias,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021000,52021000,Eventos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021100,52021100,Seguros,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021200,52021200,Servicios de Mensajería,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021300,52021300,Flete y Carga,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021400,52021400,Cuotas y Suscripciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52021500,52021500,Otros Gastos de Administración,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5202" +do_niif_52030101,52030101,Legales (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030102,52030102,Contabilidad y Auditoría (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030103,52030103,Tecnología (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030104,52030104,Mantenimiento de Planta (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030105,52030105,Mantenimiento del Local (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030106,52030106,Mantenimiento Mobiliario y Equipos (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030107,52030107,Asesorías (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030108,52030108,Fumigaciones (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030109,52030109,Copias y Escaneos (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030110,52030110,Servicios de Vigilancia (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030111,52030111,Otros Servicios Profesionales (P. Física),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520301" +do_niif_52030201,52030201,Legales (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030202,52030202,Contabilidad y Auditoría (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030203,52030203,Tecnología (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030204,52030204,Mantenimiento de Planta (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030205,52030205,Mantenimiento del Local (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030206,52030206,Mantenimiento Mobiliario y Equipos (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030207,52030207,Asesorías (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030208,52030208,Fumigaciones (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030209,52030209,Copias y Escaneos (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030210,52030210,Servicios de Vigilancia (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030211,52030211,Otros Servicios Profesionales (P. Jurídica),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520302" +do_niif_52030301,52030301,Honorarios por Servicios del Exterior - Relacionadas,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520303" +do_niif_52030302,52030302,Honorarios por Servicios del Exterior - Terceros,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5203,account_tag_520303" +do_niif_52040100,52040100,Gastos por Depreciación de Edificios,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52040200,52040200,Gastos por Depreciación de Equipo de Transporte,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52040300,52040300,Gastos por Depreciación de Mobiliario y Equipos,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52040400,52040400,Gastos por Depreciación de Maquinaria,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52040500,52040500,Gastos por Depreciación de Herramientas,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52040600,52040600,Gastos por Depreciación de Instalaciones,account.data_account_type_depreciation,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5204" +do_niif_52050100,52050100,Gastos por Reparación de Edificios,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52050200,52050200,Gastos por Reparación de Equipo de Transporte,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52050300,52050300,Gastos por Reparación de Mobiliario y Equipos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52050400,52050400,Gastos por Reparación de Maquinaria,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52050500,52050500,Gastos por Reparación de Herramientas,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52050600,52050600,Gastos por Reparación de Instalaciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5205" +do_niif_52060100,52060100,Relaciones Públicas,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060200,52060200,Publicidad,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060300,52060300,Viajes,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060400,52060400,Donaciones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060500,52060500,Donaciones a ProIndustria (Ley 392-07),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060600,52060600,Promociones,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52060700,52060700,Otros Gastos de Representación,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5206" +do_niif_52070100,52070100,Gastos por Préstamos Financieros,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070200,52070200,Retención por Cheques y Transferencias Electrónicas (0.15%),account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070300,52070300,Intereses Bancarios,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070400,52070400,Comisiones Bancarias,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070500,52070500,Comisión de Tarjeta de Crédito,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070600,52070600,Nota de Cargos por Banco,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070700,52070700,Seguro sobre Préstamos Bancarios,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070800,52070800,Pérdidas por Diferencia Cambiaria,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52070900,52070900,Otro Gastos Financieros,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5207" +do_niif_52080100,52080100,Gastos por Siniestros,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080200,52080200,Pérdidas por Ventas de Activos Fijos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080300,52080300,Pérdidas por Cuentas Incobrables,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080400,52080400,Gastos por ISR,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080500,52080500,Impuestos a los Activos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080600,52080600,Penalidades/Recargos de DGII,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080700,52080700,Penalidades/Recargos de TSS,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080800,52080800,Gastos Sin Comprobante,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_52080900,52080900,Gastos por Impuestos,account.data_account_type_expenses,FALSE,do_chart_template,"account_tag_5,account_tag_52,account_tag_5208" +do_niif_61010100,61010100,Pérdidas y Ganancias,account.data_unaffected_earnings,FALSE,do_chart_template,"account_tag_6,account_tag_61,account_tag_6101" diff --git a/addons/l10n_do/data/account.tax.template.xml b/addons/l10n_do/data/account.tax.template.xml index 166bb85294b..10e6c6edc69 100644 --- a/addons/l10n_do/data/account.tax.template.xml +++ b/addons/l10n_do/data/account.tax.template.xml @@ -183,7 +183,7 @@ <field eval="0" name="amount"/> <field name="amount_type">group</field> <field name="type_tax_use">purchase</field> - <field eval="[(6, 0, [ref('tax_10_telco'), ref('tax_2_telco')])]" name="children_tax_ids"/> + <field eval="[(6, 0, [ref('tax_18_purch_serv'), ref('tax_10_telco'), ref('tax_2_telco')])]" name="children_tax_ids"/> </record> <record id="ret_100_tax_nonprofit" model="account.tax.template"> <field name="chart_template_id" ref="do_chart_template"/> diff --git a/addons/l10n_do/data/account_account_tag_data.xml b/addons/l10n_do/data/account_account_tag_data.xml index 7e95735f9b3..db5bf0864ac 100644 --- a/addons/l10n_do/data/account_account_tag_data.xml +++ b/addons/l10n_do/data/account_account_tag_data.xml @@ -2,9 +2,562 @@ <odoo> <data noupdate="1"> - <!-- Account Tags --> + <record id='account_tag_1' model='account.account.tag'> + <field name='name'>1 Activos</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_11' model='account.account.tag'> + <field name='name'>11 Activos Corrientes</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1101' model='account.account.tag'> + <field name='name'>1101 Efectivo y Equivalentes de Efectivo</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110101' model='account.account.tag'> + <field name='name'>110101 Caja</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110102' model='account.account.tag'> + <field name='name'>110102 Bancos</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110103' model='account.account.tag'> + <field name='name'>110103 Inversiones Temporales Plazo Menor 90 días</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1102' model='account.account.tag'> + <field name='name'>1102 Inversiones en Valores a Corto Plazo</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1103' model='account.account.tag'> + <field name='name'>1103 Cuentas y Documentos por Cobrar</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110301' model='account.account.tag'> + <field name='name'>110301 Documentos por Cobrar</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110302' model='account.account.tag'> + <field name='name'>110302 Cuentas por Cobrar</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1104' model='account.account.tag'> + <field name='name'>1104 Provisión para Cuentas Incobrables</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1105' model='account.account.tag'> + <field name='name'>1105 Inventarios</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1106' model='account.account.tag'> + <field name='name'>1106 Deterioro Acumulado de Valor de Inventarios</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1107' model='account.account.tag'> + <field name='name'>1107 Estimación por Obsolecencia de Inventario</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1108' model='account.account.tag'> + <field name='name'>1108 Impuestos Adelantados</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110801' model='account.account.tag'> + <field name='name'>110801 ITBIS Pagado en Compras</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_110803' model='account.account.tag'> + <field name='name'>110803 Otros Impuestos y Saldos</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1109' model='account.account.tag'> + <field name='name'>1109 Inversiones Temporales</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1110' model='account.account.tag'> + <field name='name'>1110 Pagos Anticipados</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_12' model='account.account.tag'> + <field name='name'>12 Activos Fijos</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1201' model='account.account.tag'> + <field name='name'>1201 Propiedades, Planta y Equipo</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_120101' model='account.account.tag'> + <field name='name'>120101 Bienes Inmuebles (CAT 1)</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_120102' model='account.account.tag'> + <field name='name'>120102 Bienes Muebles (CAT 2)</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_120103' model='account.account.tag'> + <field name='name'>120103 Bienes Muebles (CAT 3)</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1202' model='account.account.tag'> + <field name='name'>1202 Depreciación Acumulada de Propiedades, Planta y Equipo</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1203' model='account.account.tag'> + <field name='name'> 1203 Deterioro de Valor Acumulado de Propiedades, Planta y Equipo</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1204' model='account.account.tag'> + <field name='name'>1204 Activos Intangibles</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1205' model='account.account.tag'> + <field name='name'>1205 Bienes en Arrendamiento</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1206' model='account.account.tag'> + <field name='name'>1206 Depreciación Acumulada de Bienes en Arrendamiento</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1207' model='account.account.tag'> + <field name='name'>1207 Deterioro de Valor Acumulado de Bienes en Arrendamiento</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1208' model='account.account.tag'> + <field name='name'>1208 Inversiones Permanentes</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_1209' model='account.account.tag'> + <field name='name'>1209 Activos Diferidos</field> + <field name='color'>1</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2' model='account.account.tag'> + <field name='name'>2 Pasivos</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_21' model='account.account.tag'> + <field name='name'>21 Pasivo Corriente</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2101' model='account.account.tag'> + <field name='name'>2101 Cuentas y Documentos por Pagar a Corto Plazo</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2102' model='account.account.tag'> + <field name='name'>2102 Beneficios por Pagar a Corto Plazo</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2103' model='account.account.tag'> + <field name='name'>2103 Impuestos y Retenciones</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_210301' model='account.account.tag'> + <field name='name'>210301 ITBIS por Pagar</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_210302' model='account.account.tag'> + <field name='name'>210302 ITBIS Retenido</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_210303' model='account.account.tag'> + <field name='name'>210303 ISR Retenido</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_210304' model='account.account.tag'> + <field name='name'>210304 Retenciones en Nómina</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_210305' model='account.account.tag'> + <field name='name'>210305 Otros Impuestos o Retenciones</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2104' model='account.account.tag'> + <field name='name'>2104 Provisiones a Corto Plazo</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2105' model='account.account.tag'> + <field name='name'>2105 Tarjetas de Crédito</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_22' model='account.account.tag'> + <field name='name'>22 Pasivo No Corriente</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2201' model='account.account.tag'> + <field name='name'>2201 Cuentas y Documentos por Pagar a Largo Plazo</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2202' model='account.account.tag'> + <field name='name'>2202 Provisión para Obligaciones Laborales</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2203' model='account.account.tag'> + <field name='name'>2203 Anticipos y Garantías de Clientes</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_2204' model='account.account.tag'> + <field name='name'>2204 Provisiones a Largo Plazo</field> + <field name='color'>8</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3' model='account.account.tag'> + <field name='name'>3 Capital</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_31' model='account.account.tag'> + <field name='name'>31 Capital Contable</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3101' model='account.account.tag'> + <field name='name'>3101 Capital Social</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3102' model='account.account.tag'> + <field name='name'>3102 Superávit por Revaluación de Activos</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_32' model='account.account.tag'> + <field name='name'>32 Utilidades Restringidas</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3201' model='account.account.tag'> + <field name='name'>3201 Reserva Legal</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_33' model='account.account.tag'> + <field name='name'>33 Resultados</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3301' model='account.account.tag'> + <field name='name'>3301 Resultados Acumulados</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3302' model='account.account.tag'> + <field name='name'>3302 Resultados del Ejercicio</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_3304' model='account.account.tag'> + <field name='name'>3304 Otras Reservas de Patrimonio</field> + <field name='color'>6</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4' model='account.account.tag'> + <field name='name'>4 Ingresos y Ganancias</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_41' model='account.account.tag'> + <field name='name'>41 Ingresos por Operaciones</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4101' model='account.account.tag'> + <field name='name'>4101 Ventas de Bienes</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4102' model='account.account.tag'> + <field name='name'>4102 Ventas de Servicios</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4103' model='account.account.tag'> + <field name='name'>4103 Devoluciones</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4104' model='account.account.tag'> + <field name='name'>4104 Descuentos</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_42' model='account.account.tag'> + <field name='name'>42 Ingresos No Operacionales</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4201' model='account.account.tag'> + <field name='name'>4201 Intereses Ganados</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4202' model='account.account.tag'> + <field name='name'>4202 Ventas de Activos</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4203' model='account.account.tag'> + <field name='name'>4203 Dividendos Ganados</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_4204' model='account.account.tag'> + <field name='name'>4204 Ingresos Extraordinarios</field> + <field name='color'>7</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5' model='account.account.tag'> + <field name='name'>5 Costos, Gastos y Pérdidas</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_51' model='account.account.tag'> + <field name='name'>51 Costos y Gastos de Operación</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5101' model='account.account.tag'> + <field name='name'>5101 Costos de Ventas</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5102' model='account.account.tag'> + <field name='name'>5102 Costos de Producción</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_52' model='account.account.tag'> + <field name='name'>52 Gastos de Operación</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5201' model='account.account.tag'> + <field name='name'>5201 Gastos de Personal</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_520101' model='account.account.tag'> + <field name='name'>520101 Aportes a la Seguridad Social</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_520102' model='account.account.tag'> + <field name='name'>520102 Otras Cargas Patronales</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5202' model='account.account.tag'> + <field name='name'>5202 Gastos de Administración</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5203' model='account.account.tag'> + <field name='name'>5203 Gastos por Trabajo, Suministros y Servicios</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_520301' model='account.account.tag'> + <field name='name'>520301 Gastos Honorarios por Servicios Profesionales (P. Física)</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_520302' model='account.account.tag'> + <field name='name'>520302 Gastos Honorarios por Servicios Profesionales (P. Jurídica)</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_520303' model='account.account.tag'> + <field name='name'>520303 Gastos Honorarios por Servicios Profesionales (P. Jurídica)</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5204' model='account.account.tag'> + <field name='name'>5204 Gastos por Depreciación</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5205' model='account.account.tag'> + <field name='name'>5205 Gastos por Reparaciones</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5206' model='account.account.tag'> + <field name='name'>5206 Gastos de Representación</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5207' model='account.account.tag'> + <field name='name'>5207 Gastos Financieros</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_5208' model='account.account.tag'> + <field name='name'>5208 Gastos Extraordinarios</field> + <field name='color'>4</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_6' model='account.account.tag'> + <field name='name'>6 Cuentas Liquidadoras de Resultados</field> + <field name='color'>5</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_61' model='account.account.tag'> + <field name='name'>61 Cuenta Liquidadora</field> + <field name='color'>5</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_6101' model='account.account.tag'> + <field name='name'>6101 Pérdidas y Ganancias</field> + <field name='color'>5</field> + <field name='applicability'>accounts</field> + </record> + + <record id='account_tag_6102' model='account.account.tag'> + <field name='name'>6102 Gastos por Impuestos</field> + <field name='color'>5</field> + <field name='applicability'>accounts</field> + </record> + + <!-- Tax Tags --> + <record id="tag_R_ISR_10" model="account.account.tag"> <field name="name">R-ISR 10%</field> <field name="applicability">taxes</field> diff --git a/addons/l10n_do/data/account_chart_template_data.xml b/addons/l10n_do/data/account_chart_template_data.xml index 0123c1d897b..696cae7b228 100644 --- a/addons/l10n_do/data/account_chart_template_data.xml +++ b/addons/l10n_do/data/account_chart_template_data.xml @@ -4,12 +4,12 @@ <record id="do_chart_template" model="account.chart.template"> <field name="property_account_receivable_id" ref="do_niif_11030201"/> <field name="property_account_payable_id" ref="do_niif_21010200"/> - <field name="property_account_expense_categ_id" ref="do_niif_52021500"/> + <field name="property_account_expense_categ_id" ref="do_niif_51010100"/> <field name="property_account_income_categ_id" ref="do_niif_41010100"/> <field eval="False" name="use_anglo_saxon"/> - <field name="property_stock_account_input_categ_id" ref="do_niif_11050300"/> - <field name="property_stock_account_output_categ_id" ref="do_niif_11050300"/> - <field name="property_stock_valuation_account_id" ref="do_niif_11060100"/> + <field name="property_stock_account_input_categ_id" ref="do_niif_21021200"/> + <field name="property_stock_account_output_categ_id" ref="do_niif_11050600"/> + <field name="property_stock_valuation_account_id" ref="do_niif_11050100"/> <field name="expense_currency_exchange_account_id" ref="do_niif_52070800"/> <field name="income_currency_exchange_account_id" ref="do_niif_42040100"/> </record> diff --git a/addons/l10n_do/data/l10n_do_chart_data.xml b/addons/l10n_do/data/l10n_do_chart_data.xml index 9075d92a621..e08e4a38ae4 100644 --- a/addons/l10n_do/data/l10n_do_chart_data.xml +++ b/addons/l10n_do/data/l10n_do_chart_data.xml @@ -16,7 +16,6 @@ <field name="bank_account_code_prefix">110102</field> <field name="transfer_account_id" ref="cash_transfer_account"/> <field name="currency_id" ref="base.DOP"/> - <field name="spoken_languages" eval="'es_DO'"/> </record> <record id="cash_transfer_account" model="account.account.template"> <field name="chart_template_id" ref="do_chart_template"/> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index 8bd16cf1dc7..6cf8c3d0589 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -26,6 +26,8 @@ <field name="refund_account_id" ref="cuenta209_01"/> <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta208_01"/> </record> <record id="tax12" model="account.tax.template"> @@ -39,6 +41,8 @@ <field name="refund_account_id" ref="cuenta209_01"/> <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta208_01"/> </record> <record id="tax1" model="account.tax.template"> @@ -51,6 +55,8 @@ <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta216_10_20"/> </record> <record id="tax2" model="account.tax.template"> @@ -63,6 +69,8 @@ <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta216_10_20"/> </record> <record id="tax3" model="account.tax.template"> @@ -99,6 +107,8 @@ <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta216_10_20"/> </record> <record id="tax8" model="account.tax.template"> @@ -111,6 +121,8 @@ <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta216_10_20"/> </record> <record id="tax13" model="account.tax.template"> @@ -123,6 +135,8 @@ <field name="account_id" ref="cuenta119_01"/> <field name="refund_account_id" ref="cuenta119_01"/> <field name="tax_group_id" ref="tax_group_iva"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta118_01"/> </record> <record id="tax14" model="account.tax.template"> @@ -135,6 +149,8 @@ <field name="account_id" ref="cuenta119_01"/> <field name="refund_account_id" ref="cuenta119_01"/> <field name="tax_group_id" ref="tax_group_iva"/> + <field name="use_cash_basis" eval="True"/> + <field name="cash_basis_account" ref="cuenta118_01"/> </record> </data> </odoo> diff --git a/addons/l10n_mx/data/fiscal_position_data.xml b/addons/l10n_mx/data/fiscal_position_data.xml index 9b12c53cd4a..4247460e340 100644 --- a/addons/l10n_mx/data/fiscal_position_data.xml +++ b/addons/l10n_mx/data/fiscal_position_data.xml @@ -1,28 +1,108 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <data noupdate="1"> - <record id="account_fiscal_position_personasmoralesdelrgimengeneral0" model="account.fiscal.position.template"> - <field name="name">Personas morales del régimen general</field> + <record id="account_fiscal_position_601_fr" model="account.fiscal.position.template"> + <field name="name">601 - General de Ley Personas Morales</field> <field name="chart_template_id" ref="mx_coa"/> </record> - <record id="account_fiscal_position_personasmoralesconfinesnolucrativos0" model="account.fiscal.position.template"> - <field name="name">Personas morales con fines no lucrativos</field> + <record id="account_fiscal_position_603_fr" model="account.fiscal.position.template"> + <field name="name">603 - Personas Morales con Fines no Lucrativos</field> <field name="chart_template_id" ref="mx_coa"/> </record> - <record id="account_fiscal_position_asociacionesreligiosas0" model="account.fiscal.position.template"> - <field name="name">Asociaciones Religiosas</field> + <record id="account_fiscal_position_605_fr" model="account.fiscal.position.template"> + <field name="name">605 - Sueldos y Salarios e Ingresos Asimilados a Salarios</field> <field name="chart_template_id" ref="mx_coa"/> </record> - <record id="account_fiscal_position_personasmoralesdelrgimensimplificado0" model="account.fiscal.position.template"> - <field name="name">Personas morales del régimen simplificado</field> + <record id="account_fiscal_position_606_fr" model="account.fiscal.position.template"> + <field name="name">606 - Arrendamiento</field> <field name="chart_template_id" ref="mx_coa"/> </record> - <record id="account_fiscal_position_personafisicaconactividadempresarial0" model="account.fiscal.position.template"> - <field name="name">Persona física con actividad empresarial</field> + <record id="account_fiscal_position_607_fr" model="account.fiscal.position.template"> + <field name="name">607 - Régimen de Enajenación o Adquisición de Bienes</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_608_fr" model="account.fiscal.position.template"> + <field name="name">608 - Demás ingresos</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_609_fr" model="account.fiscal.position.template"> + <field name="name">609 - Consolidación</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_610_fr" model="account.fiscal.position.template"> + <field name="name">610 - Residentes en el Extranjero sin Establecimiento Permanente en México</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_611_fr" model="account.fiscal.position.template"> + <field name="name">611 - Ingresos por Dividendos (socios y accionistas)</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_612_fr" model="account.fiscal.position.template"> + <field name="name">612 - Personas Físicas con Actividades Empresariales y Profesionales</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_614_fr" model="account.fiscal.position.template"> + <field name="name">614 - Ingresos por intereses</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_615_fr" model="account.fiscal.position.template"> + <field name="name">615 - Régimen de los ingresos por obtención de premios</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_616_fr" model="account.fiscal.position.template"> + <field name="name">616 - Sin obligaciones fiscales</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_620_fr" model="account.fiscal.position.template"> + <field name="name">620 - Sociedades Cooperativas de Producción que optan por diferir sus ingresos</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_621_fr" model="account.fiscal.position.template"> + <field name="name">621 - Incorporación Fiscal</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_622_fr" model="account.fiscal.position.template"> + <field name="name">622 - Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_623_fr" model="account.fiscal.position.template"> + <field name="name">623 - Opcional para Grupos de Sociedades</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_624_fr" model="account.fiscal.position.template"> + <field name="name">624 - Coordinados</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_628_fr" model="account.fiscal.position.template"> + <field name="name">628 - Hidrocarburos</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_629_fr" model="account.fiscal.position.template"> + <field name="name">629 - De los Regímenes Fiscales Preferentes y de las Empresas Multinacionales</field> + <field name="chart_template_id" ref="mx_coa"/> + </record> + + <record id="account_fiscal_position_630_fr" model="account.fiscal.position.template"> + <field name="name">630 - Enajenación de acciones en bolsa de valores</field> <field name="chart_template_id" ref="mx_coa"/> </record> diff --git a/addons/l10n_mx/models/chart_template.py b/addons/l10n_mx/models/chart_template.py index 155877b2761..4e1397073ba 100644 --- a/addons/l10n_mx/models/chart_template.py +++ b/addons/l10n_mx/models/chart_template.py @@ -8,44 +8,6 @@ from odoo import models, api, _ class AccountChartTemplate(models.Model): _inherit = "account.chart.template" - @api.multi - def _load_template( - self, company, code_digits=None, transfer_account_id=None, - account_ref=None, taxes_ref=None): - """ - Set the 'use_cash_basis' and 'cash_basis_account' fields on account.account. This hack is needed due to the fact - that the tax template does not have the fields 'use_cash_basis' and 'cash_basis_account'. - - This hunk should be removed in master, as the account_tax_cash_basis module has been merged already in account - module - """ - self.ensure_one() - accounts, taxes = super(AccountChartTemplate, self)._load_template( - company, code_digits=code_digits, - transfer_account_id=transfer_account_id, account_ref=account_ref, - taxes_ref=taxes_ref) - if not self == self.env.ref('l10n_mx.mx_coa'): - return accounts, taxes - account_tax_obj = self.env['account.tax'] - account_obj = self.env['account.account'] - taxes_acc = { - 'IVA': account_obj.search([('code', '=', '208.01.01')]), - 'ITAXR_04-OUT': account_obj.search([('code', '=', '216.10.20')]), - 'ITAXR_10-OUT': account_obj.search([('code', '=', '216.10.20')]), - 'ITAX_1067-OUT': account_obj.search([('code', '=', '216.10.20')]), - 'ITAX_167-OUT': account_obj.search([('code', '=', '216.10.20')]), - 'ITAX_010-OUT': account_obj.search([('code', '=', '208.01.01')]), - 'ITAX_160-OUT': account_obj.search([('code', '=', '208.01.01')])} - - for tax in self.tax_template_ids: - if tax.description not in taxes_acc: - continue - account_tax_obj.browse(taxes.get(tax.id)).write({ - 'use_cash_basis': True, - 'cash_basis_account': taxes_acc.get(tax.description).id, - }) - return accounts, taxes - @api.model def generate_journals(self, acc_template_ref, company, journals_dict=None): """Set the tax_cash_basis_journal_id on the company""" diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 02cb12c07c7..479838ec9cb 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -215,11 +215,12 @@ class Partner(models.Model): (not self.pool._init or test_mode): email_ids = emails.ids dbname = self.env.cr.dbname + _context = self._context def send_notifications(): db_registry = registry(dbname) with api.Environment.manage(), db_registry.cursor() as cr: - env = api.Environment(cr, SUPERUSER_ID, {}) + env = api.Environment(cr, SUPERUSER_ID, _context) env['mail.mail'].browse(email_ids).send() # unless asked specifically, send emails after the transaction to diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index c650217192f..d0d918664fe 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -39,7 +39,7 @@ <separator/> <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/> <separator/> - <filter string="Archived" name="inactive" domain="[('active','=',False)]"/> + <filter string="Archived" name="inactive" domain="[('archive','=',True)]"/> <group expand='0' string='Group by...'> <filter string='Assigned' domain="[]" context="{'group_by': 'technician_user_id'}"/> <filter string='Category' domain="[]" context="{'group_by' : 'category_id'}"/> diff --git a/addons/marketing_campaign/models/marketing_campaign.py b/addons/marketing_campaign/models/marketing_campaign.py index 0d6f20a0506..7370633114e 100644 --- a/addons/marketing_campaign/models/marketing_campaign.py +++ b/addons/marketing_campaign/models/marketing_campaign.py @@ -442,7 +442,7 @@ class MarketingCampaignWorkitem(models.Model): for workitem in self: proxy = self.env[workitem.object_id.model] record = proxy.browse(workitem.res_id) - if not workitem.res_id or not record: + if not workitem.res_id or not record.exists(): workitem.res_name = '/' continue workitem.res_name = record.name_get()[0][1] diff --git a/addons/mass_mailing/data/mass_mailing_data.xml b/addons/mass_mailing/data/mass_mailing_data.xml index 2e165a52806..f92fa8e9036 100644 --- a/addons/mass_mailing/data/mass_mailing_data.xml +++ b/addons/mass_mailing/data/mass_mailing_data.xml @@ -21,5 +21,17 @@ <field name="name">Newsletter</field> </record> + <!-- Cron that process the mass mailing queue --> + <record id="ir_cron_mass_mailing_queue" model="ir.cron"> + <field name="name">Mass Mailing: Process queue</field> + <field name="model_id" ref="model_mail_mass_mailing"/> + <field name="state">code</field> + <field name="code">model._process_mass_mailing_queue()</field> + <field name="user_id" ref="base.user_root" /> + <field name="interval_number">60</field> + <field name="interval_type">minutes</field> + <field name="numbercall">-1</field> + <field eval="False" name="doall" /> + </record> </data> </odoo> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 633a11e1e74..17343a91a0e 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -500,7 +500,7 @@ class MassMailing(models.Model): res = [x for x in read_group_res if x['state'] == state_value] if not res: res = [x for x in read_group_all_states if x['state'] == state_value] - res[0]['state'] = [state_value, state_name] + res[0]['state'] = state_value result.append(res[0]) return result else: diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less index 74f71191db2..cfd1264e0c7 100644 --- a/addons/mass_mailing/static/src/less/themes/theme_default.less +++ b/addons/mass_mailing/static/src/less/themes/theme_default.less @@ -55,9 +55,12 @@ background-color: @o_mm_def_color_alpha; padding: 24px 3%; - // Forces on <p/> elements as several mail clients does not correctly - // inherit font properties + // Forces on <p/> and <hx/> elements as several mail clients does not + // correctly inherit font properties &, p { + font-size: 14px; // force bootstrap default + } + &, p, h1, h2, h3, h4, h5, h6 { font-family: @o_mm_def_font; color: lighten(@o_mm_def_text_color, 20%); } diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index 123dd3d7e72..17d9818077c 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -861,19 +861,6 @@ </field> </record> - <!-- Cron that process the mass mailing queue --> - <record id="ir_cron_mass_mailing_queue" model="ir.cron"> - <field name="name">Mass Mailing: Process queue</field> - <field name="model_id" ref="model_mail_mass_mailing"/> - <field name="state">code</field> - <field name="code">model._process_mass_mailing_queue()</field> - <field name="user_id" ref="base.user_root" /> - <field name="interval_number">60</field> - <field name="interval_type">minutes</field> - <field name="numbercall">-1</field> - <field eval="False" name="doall" /> - </record> - <menuitem name="Campaigns" id="menu_email_campaigns" parent="mass_mailing_menu_root" sequence="5" action="action_view_mass_mailing_campaigns" diff --git a/addons/membership/wizard/membership_invoice.py b/addons/membership/wizard/membership_invoice.py index d516eb821b1..56208de5abf 100644 --- a/addons/membership/wizard/membership_invoice.py +++ b/addons/membership/wizard/membership_invoice.py @@ -30,12 +30,13 @@ class MembershipInvoice(models.TransientModel): search_view_ref = self.env.ref('account.view_account_invoice_filter', False) form_view_ref = self.env.ref('account.invoice_form', False) + tree_view_ref = self.env.ref('account.invoice_tree', False) return { 'domain': [('id', 'in', invoice_list)], 'name': 'Membership Invoices', 'res_model': 'account.invoice', 'type': 'ir.actions.act_window', - 'views': [(False, 'tree'), (form_view_ref and form_view_ref.id, 'form')], + 'views': [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')], 'search_view_id': search_view_ref and search_view_ref.id, } diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 31cd5226346..75d56a0c958 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -77,10 +77,9 @@ class MrpUnbuild(models.Model): @api.model def create(self, vals): - if not vals.get('name'): + if vals['name'] == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('mrp.unbuild') or _('New') - unbuild = super(MrpUnbuild, self).create(vals) - return unbuild + return super(MrpUnbuild, self).create(vals) @api.multi def action_unbuild(self): diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 1fee94e7f5f..b115fc2c16e 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -154,7 +154,7 @@ class StockMove(models.Model): lots = self.env['stock.move.lots'] for move in self: unlink_move_lots = move.move_lot_ids.filtered(lambda x : (x.quantity_done == 0) and not x.workorder_id) - unlink_move_lots.unlink() + unlink_move_lots.sudo().unlink() group_new_quant = {} old_move_lot = {} for movelot in move.move_lot_ids: diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index 9a6f777a23e..979eff2ccf9 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -40,7 +40,8 @@ </div> <div class="col-xs-3"> <strong>Printing date:</strong><br/> - <span t-esc="time.strftime('%Y-%m-%d')"/> + <span t-esc="time.strftime('%Y-%m-%d')" + t-options='{"widget": "date"}'/> </div> <div class="col-xs-3" t-if="'sale_ref' in o"> <strong>Partner Ref:</strong><br/> diff --git a/addons/mrp/static/src/xml/mrp.xml b/addons/mrp/static/src/xml/mrp.xml index 54b1bb6fc9b..5a1c9353084 100644 --- a/addons/mrp/static/src/xml/mrp.xml +++ b/addons/mrp/static/src/xml/mrp.xml @@ -16,8 +16,10 @@ </div> </t> <t t-extend="HiddenInputFile" t-name="HiddenInputPDFFile"> - <t t-jquery="input[type='file']:first" t-operation="replace"> - <input type="file" class="o_input_file" name="ufile" accept="application/pdf" /> + <!-- remove the inputs and put a new one for pdf only --> + <t t-jquery="input[type='file']" t-operation="replace"/> + <t t-jquery="form" t-operation="append"> + <input type="file" class="o_input_file" name="ufile" accept="application/pdf"/> </t> </t> </templates> diff --git a/addons/mrp/static/tests/mrp_tests.js b/addons/mrp/static/tests/mrp_tests.js index ed4dab4f91b..40f6fc2607b 100644 --- a/addons/mrp/static/tests/mrp_tests.js +++ b/addons/mrp/static/tests/mrp_tests.js @@ -31,7 +31,7 @@ QUnit.module('mrp', { }, }, function () { QUnit.test("pdf_viewer without data", function (assert) { - assert.expect(3); + assert.expect(4); var form = createView({ View: FormView, @@ -48,6 +48,8 @@ QUnit.module('mrp', { "there should be a visible 'Upload' button"); assert.ok(form.$('.o_field_widget iframe.o_pdfview_iframe').hasClass('o_hidden'), "there should be an invisible iframe"); + assert.strictEqual(form.$('input[type="file"]').length, 1, + "there should be one input"); form.destroy(); }); diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 8ff73c58c0f..01c474d7bc3 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -47,7 +47,7 @@ <button name="action_see_attachments" type="object" icon="fa-file" attrs="{'invisible': [('has_attachments', '=', True)]}"/> <field name="product_qty"/> <field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="product.group_uom"/> - <field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" groups="product.group_product_variant"/> + <field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" options="{'no_create':True}" groups="product.group_product_variant"/> <field name="operation_id" groups="mrp.group_mrp_routings" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/> </tree> </field> diff --git a/addons/payment/models/account_payment.py b/addons/payment/models/account_payment.py index d507666f35f..60ddcc60b4d 100644 --- a/addons/payment/models/account_payment.py +++ b/addons/payment/models/account_payment.py @@ -39,8 +39,8 @@ class AccountPayment(models.Model): def _do_payment(self): if self.payment_token_id.acquirer_id.auto_confirm == 'authorize': - raise ValidationError('This feature is not available for payment acquirers set to the "Authorize" mode.\n' - 'Please use a token from another provider than %s.' % self.payment_token_id.acquirer_id.name) + raise ValidationError(_('This feature is not available for payment acquirers set to the "Authorize" mode.\n' + 'Please use a token from another provider than %s.') % self.payment_token_id.acquirer_id.name) reference = "P-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) tx = self.env['payment.transaction'].create({ 'amount': self.amount, diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index a8e3f895c9f..92f0da0bc74 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -162,7 +162,7 @@ class PaymentAcquirer(models.Model): def _check_authorization_support(self): for acquirer in self: if acquirer.auto_confirm == 'authorize' and acquirer.provider not in self._get_feature_support()['authorize']: - raise ValidationError('You cannot capture payments manually with this payment method. Please choose another Order Confirmation mode (in Configuration tab).') + raise ValidationError(_('You cannot capture payments manually with this payment method. Please choose another Order Confirmation mode (in Configuration tab).')) return True _constraints = [ @@ -664,14 +664,14 @@ class PaymentTransaction(models.Model): @api.multi def action_capture(self): if any(self.mapped(lambda tx: tx.state != 'authorized')): - raise ValidationError('Only transactions in the Authorized status can be captured.') + raise ValidationError(_('Only transactions in the Authorized status can be captured.')) for tx in self: tx.s2s_capture_transaction() @api.multi def action_void(self): if any(self.mapped(lambda tx: tx.state != 'authorized')): - raise ValidationError('Only transactions in the Authorized status can be voided.') + raise ValidationError(_('Only transactions in the Authorized status can be voided.')) for tx in self: tx.s2s_void_transaction() diff --git a/addons/payment_authorize/i18n/payment_authorize.pot b/addons/payment_authorize/i18n/payment_authorize.pot index f155720d7d6..373fadac77b 100644 --- a/addons/payment_authorize/i18n/payment_authorize.pot +++ b/addons/payment_authorize/i18n/payment_authorize.pot @@ -26,11 +26,24 @@ msgid "API Transaction Key" msgstr "" #. module: payment_authorize +#: code:addons/payment_authorize/models/authorize_request.py:66 +#, python-format +msgid "Authorize.net Error Message(s):\n" +" %s" +msgstr "" + +#. module: payment_authorize #: model:ir.model.fields,field_description:payment_authorize.field_payment_token_authorize_profile msgid "Authorize.net Profile ID" msgstr "" #. module: payment_authorize +#: code:addons/payment_authorize/models/payment.py:167 +#, python-format +msgid "Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)" +msgstr "" + +#. module: payment_authorize #: model:ir.ui.view,arch_db:payment_authorize.authorize_s2s_form msgid "CVC" msgstr "" @@ -76,6 +89,12 @@ msgid "Payment Transaction" msgstr "" #. module: payment_authorize +#: code:addons/payment_authorize/models/payment.py:347 +#, python-format +msgid "The Customer Profile creation in Authorize.NET failed." +msgstr "" + +#. module: payment_authorize #: model:ir.model.fields,help:payment_authorize.field_payment_token_authorize_profile msgid "This contains the unique reference for this partner/payment token combination in the Authorize.net backend" msgstr "" diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index fee45016140..f312add67b3 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -62,7 +62,7 @@ class AuthorizeAPI(): response = strip_ns(r.content, XMLNS) if response.find('messages/resultCode').text == 'Error': messages = [m.text for m in response.findall('messages/message/text')] - raise ValidationError('Authorize.net Error Message(s):\n %s' % '\n'.join(messages)) + raise ValidationError(_('Authorize.net Error Message(s):\n %s') % '\n'.join(messages)) return response def _base_tree(self, requestType): diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 9134549b393..a80627dbf35 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -164,7 +164,7 @@ class TxAuthorize(models.Model): transaction record. """ reference, trans_id, fingerprint = data.get('x_invoice_num'), data.get('x_trans_id'), data.get('x_MD5_Hash') if not reference or not trans_id or not fingerprint: - error_msg = 'Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)' % (reference, trans_id, fingerprint) + error_msg = _('Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)') % (reference, trans_id, fingerprint) _logger.info(error_msg) raise ValidationError(error_msg) tx = self.search([('reference', '=', reference)]) @@ -343,6 +343,6 @@ class PaymentToken(models.Model): 'acquirer_ref': res.get('payment_profile_id'), } else: - raise ValidationError('The Customer Profile creation in Authorize.NET failed.') + raise ValidationError(_('The Customer Profile creation in Authorize.NET failed.')) else: return values diff --git a/addons/payment_paypal/views/payment_paypal_templates.xml b/addons/payment_paypal/views/payment_paypal_templates.xml index 495aac7158e..9ba50a2b249 100644 --- a/addons/payment_paypal/views/payment_paypal_templates.xml +++ b/addons/payment_paypal/views/payment_paypal_templates.xml @@ -20,6 +20,8 @@ <input type="hidden" name="first_name" t-att-value="first_name"/> <input type="hidden" name="last_name" t-att-value="last_name"/> <input type="hidden" name="zip" t-att-value="zip_code"/> + <input t-if='state' type='hidden' name="state" + t-att-value='state'/> <!-- after payment parameters --> <input t-if='custom' type='hidden' name="custom" t-att-value='custom'/> diff --git a/addons/payment_sips/__manifest__.py b/addons/payment_sips/__manifest__.py index a8c608d8774..6039f962009 100644 --- a/addons/payment_sips/__manifest__.py +++ b/addons/payment_sips/__manifest__.py @@ -1,22 +1,8 @@ # -*- encoding: utf-8 -*- -############################################################################## -# -# Copyright Eezee-It -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -############################################################################## +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright 2015 Eezee-It + { 'name': 'Worldline SIPS', 'version': '1.0', diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py index c5b21fbf517..3af8e2c7b3e 100644 --- a/addons/payment_sips/controllers/main.py +++ b/addons/payment_sips/controllers/main.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +# Copyright 2015 Eezee-It + import json import logging import werkzeug diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py index 51f78007d91..f01c59d1880 100644 --- a/addons/payment_sips/models/payment.py +++ b/addons/payment_sips/models/payment.py @@ -1,5 +1,7 @@ # coding: utf-8 +# Copyright 2015 Eezee-It + import json import logging from hashlib import sha256 diff --git a/addons/point_of_sale/i18n/point_of_sale.pot b/addons/point_of_sale/i18n/point_of_sale.pot index e3be0ce22a9..77c4a996ec8 100644 --- a/addons/point_of_sale/i18n/point_of_sale.pot +++ b/addons/point_of_sale/i18n/point_of_sale.pot @@ -3513,6 +3513,12 @@ msgid "You have to select a pricelist in the sale form !\n" msgstr "" #. module: point_of_sale +#: code:addons/point_of_sale/models/pos_session.py:24 +#, python-format +msgid "You have to set a Sale Journal for the POS:%s" +msgstr "" + +#. module: point_of_sale #: model:ir.actions.act_window,help:point_of_sale.product_product_action msgid "You must define a product for everything you sell through\n" " the point of sale interface." diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 0dd1afa9cde..a6a7c8d001a 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -294,7 +294,8 @@ class PosOrder(models.Model): taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id) if not taxes: continue - for tax in taxes.compute_all(line.price_unit * (100.0 - line.discount) / 100.0, cur, line.qty)['taxes']: + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + for tax in taxes.compute_all(price, cur, line.qty)['taxes']: insert_data('tax', { 'name': _('Tax') + ' ' + tax['name'], 'product_id': line.product_id.id, diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 09400db3f46..aa1f44f9ea1 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -22,6 +22,8 @@ class PosSession(models.Model): orders = session.order_ids.filtered(lambda order: order.state == 'paid') journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id) + if not journal_id: + raise UserError(_("You have to set a Sale Journal for the POS:%s") % (session.config_id.name,)) move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id) orders.with_context(force_company=company_id)._create_account_move_line(session, move) @@ -125,7 +127,7 @@ class PosSession(models.Model): action['domain'] = [('id', 'in', pickings.ids)] return action - @api.depends('config_id.cash_control') + @api.depends('config_id', 'statement_ids') def _compute_cash_all(self): for session in self: session.cash_journal_id = session.cash_register_id = session.cash_control = False @@ -135,7 +137,7 @@ class PosSession(models.Model): session.cash_control = True session.cash_journal_id = statement.journal_id.id session.cash_register_id = statement.id - if not session.cash_control: + if not session.cash_control and session.state != 'closed': raise UserError(_("Cash control can only be applied to cash journals.")) @api.constrains('user_id', 'state') diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 78b150b6844..eb7df79ec4c 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1459,6 +1459,7 @@ var ReceiptScreenWidget = ScreenWidget.extend({ print_xml: function() { var env = { widget: this, + pos: this.pos, order: this.pos.get_order(), receipt: this.pos.get_order().export_for_printing(), paymentlines: this.pos.get_order().get_paymentlines() diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index e746a6f66f0..8e24db3c167 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -753,7 +753,7 @@ <t t-if='simple'> <line> <left><t t-esc='line.product_name_wrapped[0]' /></left> - <right><value t-att-value-decimals='pos.currency.decimals'>><t t-esc='line.price_display' /></value></right> + <right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right> </line> <t t-call="XmlReceiptWrappedProductNameLines"/> </t> diff --git a/addons/point_of_sale/wizard/pos_payment.xml b/addons/point_of_sale/wizard/pos_payment.xml index 1889653fb73..a00ad3957db 100644 --- a/addons/point_of_sale/wizard/pos_payment.xml +++ b/addons/point_of_sale/wizard/pos_payment.xml @@ -7,7 +7,7 @@ <form string="Pay Order"> <group> <field name="session_id" invisible="True"/> - <field name="journal_id" /> + <field name="journal_id" domain="[('journal_user', '=', True)]"/> <field name="amount" /> <field name="payment_name"/> </group> diff --git a/addons/portal/i18n/portal.pot b/addons/portal/i18n/portal.pot index a826837ffa1..a6729e5c40a 100644 --- a/addons/portal/i18n/portal.pot +++ b/addons/portal/i18n/portal.pot @@ -91,6 +91,12 @@ msgid "Email" msgstr "" #. module: portal +#: code:addons/portal/wizard/portal_wizard.py:130 +#, python-format +msgid "Group %s is not a portal" +msgstr "" + +#. module: portal #: model:ir.model.fields,field_description:portal.field_portal_wizard_id #: model:ir.model.fields,field_description:portal.field_portal_wizard_user_id msgid "ID" diff --git a/addons/pos_mercury/i18n/pos_mercury.pot b/addons/pos_mercury/i18n/pos_mercury.pot index 9848ee5058e..2dd02dd8356 100644 --- a/addons/pos_mercury/i18n/pos_mercury.pot +++ b/addons/pos_mercury/i18n/pos_mercury.pot @@ -140,6 +140,13 @@ msgid "Could Not Encrypt Response- Call Provider" msgstr "" #. module: pos_mercury +#. openerp-web +#: code:addons/pos_mercury/static/src/js/pos_mercury.js:357 +#, python-format +msgid "Could not read card" +msgstr "" + +#. module: pos_mercury #: model:ir.model.fields,field_description:pos_mercury.field_pos_mercury_configuration_create_uid #: model:ir.model.fields,field_description:pos_mercury.field_pos_mercury_mercury_transaction_create_uid msgid "Created by" @@ -193,6 +200,13 @@ msgstr "" #. module: pos_mercury #. openerp-web +#: code:addons/pos_mercury/static/src/js/pos_mercury.js:638 +#, python-format +msgid "Error" +msgstr "" + +#. module: pos_mercury +#. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:198 #, python-format msgid "Error Occurred While Decrypting Request" @@ -595,6 +609,18 @@ msgid "Name of this Mercury configuration" msgstr "" #. module: pos_mercury +#: code:addons/pos_mercury/models/pos_mercury_transaction.py:35 +#, python-format +msgid "No Mercury configuration associated with the journal." +msgstr "" + +#. module: pos_mercury +#: code:addons/pos_mercury/models/pos_mercury_transaction.py:23 +#, python-format +msgid "No opened point of sale session for user %s found" +msgstr "" + +#. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:334 #, python-format diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index 7c2567af8df..5b691ce672b 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -7,6 +7,7 @@ import requests import werkzeug from odoo import models, api, service +from odoo.tools.translate import _ from odoo.exceptions import UserError from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, misc @@ -17,7 +18,7 @@ class MercuryTransaction(models.Model): def _get_pos_session(self): pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) if not pos_session: - raise UserError("No POS session") + raise UserError(_("No opened point of sale session for user %s found") % self.env.user.name) pos_session.login() @@ -29,7 +30,7 @@ class MercuryTransaction(models.Model): if journal and journal.pos_mercury_config_id: return journal.pos_mercury_config_id else: - raise UserError("No Mercury configuration associated with the journal.") + raise UserError(_("No Mercury configuration associated with the journal.")) def _setup_request(self, data): # todo: in master make the client include the pos.session id and use that diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js index 898fb8b4925..710420c62b4 100644 --- a/addons/pos_mercury/static/src/js/pos_mercury.js +++ b/addons/pos_mercury/static/src/js/pos_mercury.js @@ -348,7 +348,7 @@ PaymentScreenWidget.include({ if (! decodedMagtek) { this.gui.show_popup('error',{ - 'title': 'Could not read card', + 'title': _t('Could not read card'), 'body': _t('This can be caused by a badly executed swipe or by not having your keyboard layout set to US QWERTY (not US International).'), }); return; @@ -638,7 +638,7 @@ PaymentScreenWidget.include({ if (already_swipe_pending) { this.gui.show_popup('error',{ - 'title': 'Error', + 'title': _t('Error'), 'body': _t('One credit card swipe already pending.'), }); } else { diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py index bdedd9d7f9d..20465d47c49 100644 --- a/addons/product/models/product_attribute.py +++ b/addons/product/models/product_attribute.py @@ -76,7 +76,7 @@ class ProductAttributevalue(models.Model): @api.multi def _variant_name(self, variable_attributes): - return ", ".join([v.name for v in self.sorted(key=lambda r: r.name) if v.attribute_id in variable_attributes]) + return ", ".join([v.name for v in self.sorted(key=lambda r: r.attribute_id.name) if v.attribute_id in variable_attributes]) class ProductAttributePrice(models.Model): diff --git a/addons/product/models/product_uom.py b/addons/product/models/product_uom.py index 83c409f7538..9606a51dfca 100644 --- a/addons/product/models/product_uom.py +++ b/addons/product/models/product_uom.py @@ -90,6 +90,8 @@ class ProductUoM(models.Model): @api.multi def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP'): + if not self: + return qty self.ensure_one() if self.category_id.id != to_unit.category_id.id: if self._context.get('raise-exception', True): diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 60e7b46ad96..5cbbc758963 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -217,10 +217,10 @@ <field name="currency_id" invisible='1'/> </group> <group name="weight"> - <group name="weight" string="Weights"> + <group name="weight" string="Weights" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"> <label for="volume"/> <div class="o_row"> - <field digits="[14, 3]" name="volume"/> + <field name="volume"/> <span>m³</span> </div> <label for="weight"/> diff --git a/addons/product_margin/views/product_product_views.xml b/addons/product_margin/views/product_product_views.xml index c229f770eb9..8612b355a1d 100644 --- a/addons/product_margin/views/product_product_views.xml +++ b/addons/product_margin/views/product_product_views.xml @@ -7,8 +7,7 @@ <field name="arch" type="xml"> <graph string="Product Margins" type="bar"> <field name="product_tmpl_id"/> - <field name="turnover" operator="+"/> - <field name="total_cost" operator="+"/> + <field name="total_margin" type="measure" operator="+"/> </graph> </field> </record> @@ -73,7 +72,7 @@ <field name="sales_gap" sum="Sales Gap"/> <field name="total_cost" sum="Total Cost"/> <field name="purchase_num_invoiced" string="# Purchased"/> - <field name="total_margin"/> + <field name="total_margin" sum="Total Margin"/> <field name="expected_margin"/> <field name="total_margin_rate" widget="progressbar"/> <field name="expected_margin_rate" widget="progressbar"/> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 3c6d7f73759..3e1923628e1 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -350,7 +350,7 @@ action = model.activate_sample_project() <field name="res_model">project.project</field> <field name="view_type">form</field> <field name="domain">[]</field> - <field name="view_mode">list,kanban,form</field> + <field name="view_mode">tree,kanban,form</field> <field name="search_view_id" ref="view_project_project_filter"/> <field name="context">{}</field> <field name="help" type="html"> diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index 9c1cf48312c..31c1c948d5c 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -75,6 +75,8 @@ class AccountInvoice(models.Model): new_lines += new_line self.invoice_line_ids += new_lines + self.payment_term_id = self.purchase_id.payment_term_id + self.env.context = dict(self.env.context, from_purchase_order_change=True) self.purchase_id = False return {} @@ -92,7 +94,10 @@ class AccountInvoice(models.Model): @api.onchange('partner_id', 'company_id') def _onchange_partner_id(self): + payment_term_id = self.env.context.get('from_purchase_order_change') and self.payment_term_id or False res = super(AccountInvoice, self)._onchange_partner_id() + if payment_term_id: + self.payment_term_id = payment_term_id if not self.env.context.get('default_journal_id') and self.partner_id and self.currency_id and\ self.type in ['in_invoice', 'in_refund'] and\ self.currency_id != self.partner_id.property_purchase_currency_id: @@ -144,6 +149,7 @@ class AccountInvoice(models.Model): valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id) if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id: #for average/fifo/lifo costing method, fetch real cost price from incomming moves + valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id) stock_move_obj = self.env['stock.move'] valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id), ('state', '=', 'done')]) if valuation_stock_move: @@ -155,7 +161,7 @@ class AccountInvoice(models.Model): valuation_price_unit = valuation_price_unit_total / valuation_total_qty valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id) if inv.currency_id.id != company_currency.id: - valuation_price_unit = company_currency.with_context(date=inv.date_invoice).compute(valuation_price_unit, inv.currency_id) + valuation_price_unit = company_currency.with_context(date=inv.date_invoice).compute(valuation_price_unit, inv.currency_id, round=False) if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc: # price with discount and without tax included price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 3dc6e9cb614..20da880fa2f 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -579,6 +579,11 @@ class PurchaseOrderLine(models.Model): msg += _("Billed Quantity") + ": %s <br/></li>" % (line.qty_invoiced,) msg += "</ul>" order.message_post(body=msg) + # Update expected date of corresponding moves + if 'date_planned' in values: + self.env['stock.move'].search([ + ('purchase_line_id', 'in', self.ids), ('state', '!=', 'done') + ]).write({'date_expected': values['date_planned']}) result = super(PurchaseOrderLine, self).write(values) if orders: orders._create_picking() @@ -987,6 +992,21 @@ class ProcurementOrder(models.Model): """ return suppliers[0] + def _make_po_get_domain(self, partner): + gpo = self.rule_id.group_propagation_option + group = (gpo == 'fixed' and self.rule_id.group_id) or \ + (gpo == 'propagate' and self.group_id) or False + + domain = ( + ('partner_id', '=', partner.id), + ('state', '=', 'draft'), + ('picking_type_id', '=', self.rule_id.picking_type_id.id), + ('company_id', '=', self.company_id.id), + ('dest_address_id', '=', self.partner_dest_id.id)) + if group: + domain += (('group_id', '=', group.id),) + return domain + @api.multi def make_po(self): cache = {} @@ -1000,18 +1020,7 @@ class ProcurementOrder(models.Model): supplier = procurement._make_po_select_supplier(suppliers) partner = supplier.name - gpo = procurement.rule_id.group_propagation_option - group = (gpo == 'fixed' and procurement.rule_id.group_id) or \ - (gpo == 'propagate' and procurement.group_id) or False - - domain = ( - ('partner_id', '=', partner.id), - ('state', '=', 'draft'), - ('picking_type_id', '=', procurement.rule_id.picking_type_id.id), - ('company_id', '=', procurement.company_id.id), - ('dest_address_id', '=', procurement.partner_dest_id.id)) - if group: - domain += (('group_id', '=', group.id),) + domain = procurement._make_po_get_domain(partner) if domain in cache: po = cache[domain] diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 711fc67eda1..44456123e7f 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -321,7 +321,7 @@ <separator/> <filter name="not_invoiced" string="Waiting Bills" domain="[('invoice_status','=', 'to invoice')]" help="Purchase orders that include lines not invoiced."/> <filter name="invoiced" string="Bills Received" domain="[('invoice_status','=', 'invoiced')]" help="Purchase orders that have been invoiced."/> - <filter string="New Mail" name="message_unread" domain="[('message_unread','=',True)]"/> + <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/> <separator/> <filter string="My Activities" name="activities_my" domain="[('activity_ids.user_id', '=', uid)]"/> diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 25130e8b6f5..49af47b0b18 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -205,7 +205,7 @@ class PurchaseOrder(models.Model): # Compute taxes if fpos: - taxes_ids = fpos.map_tax(line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id)) + taxes_ids = fpos.map_tax(line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id)).ids else: taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index d0b11a6ac02..616bfa78f1a 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -90,6 +90,9 @@ class AccountInvoice(models.Model): self.ensure_one() return self.partner_shipping_id.id or super(AccountInvoice, self).get_delivery_partner_id() + def _get_refund_common_fields(self): + return super(AccountInvoice, self)._get_refund_common_fields() + ['team_id', 'partner_shipping_id'] + class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' _order = 'invoice_id, layout_category_id, sequence, id' diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 160f069d9dd..b3e8af8dc6e 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -53,7 +53,7 @@ class SaleOrder(models.Model): # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in # 'account.invoice.refund') # use like as origin may contains multiple references (e.g. 'SO01, SO02') - refunds = invoice_ids.search([('origin', 'like', order.name)]) + refunds = invoice_ids.search([('origin', 'like', order.name)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund']) invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')]) # Search for refunds as well refund_ids = self.env['account.invoice'].browse() diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index a201e66185e..9ec3ce6b9d5 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -134,4 +134,4 @@ class TestSaleOrder(TestSale): inv.action_invoice_open() sol = so.order_line.filtered(lambda l: l.product_id == serv_cost) self.assertTrue(sol, 'Sale: cost invoicing does not add lines when confirming vendor invoice') - self.assertTrue(sol.price_unit == 160 and sol.qty_delivered == 2 and sol.product_uom_qty == sol.qty_invoiced == 0, 'Sale: line is wrong after confirming vendor invoice') + self.assertEquals((sol.price_unit, sol.qty_delivered, sol.product_uom_qty, sol.qty_invoiced), (160, 2, 0, 0), 'Sale: line is wrong after confirming vendor invoice') diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 2b571f5df0a..2db872ad8c7 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -29,7 +29,7 @@ class StockMove(models.Model): for move in self: if move.picking_id and move.picking_id.group_id: picking = move.picking_id - order = self.env['sale.order'].search([('procurement_group_id', '=', picking.group_id.id)]) + order = self.env['sale.order'].sudo().search([('procurement_group_id', '=', picking.group_id.id)]) picking.message_post_with_view( 'mail.message_origin_link', values={'self': picking, 'origin': order}, diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index 22f82e1ed28..14fc5fd6195 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -20,21 +20,21 @@ <tbody> <tr> <th rowspan="4"> - <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_group_by': ['date:month'],'pivot_column_group_by': ['timesheet_invoice_type'], 'measures': ['unit_amount']}">Hours</a> + <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['date:month'],'pivot_column_groupby': ['timesheet_invoice_type'], 'pivot_measures': ['unit_amount']}">Hours</a> </th> <td class="o_timesheet_plan_dashboard_cell"> <t t-esc="dashboard['hours']['billable_time']"/> h </td> <td>Time and material</td> <th rowspan="4"> - <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_group_by': ['date:month', 'employee_id'], 'pivot_column_group_by': ['timesheet_invoice_type'],'pivot_measures': ['unit_amount']}">Rates</a> + <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['date:month', 'employee_id'], 'pivot_column_groupby': ['timesheet_invoice_type'],'pivot_measures': ['unit_amount']}">Rates</a> </th> <td class="o_timesheet_plan_dashboard_cell"> <t t-esc="dashboard['rates']['billable_time']"/> % </td> <td>Time and material</td> <th rowspan="4"> - <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_group_by': ['timesheet_invoice_id'], 'pivot_column_group_by': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a> + <a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a> </th> <td class="o_timesheet_plan_dashboard_cell"> <t t-if="currency.position == 'before'" t-esc="currency.symbol"/> diff --git a/addons/sales_team/views/crm_team_views.xml b/addons/sales_team/views/crm_team_views.xml index a2d25ca0c38..b4ad3372495 100644 --- a/addons/sales_team/views/crm_team_views.xml +++ b/addons/sales_team/views/crm_team_views.xml @@ -17,7 +17,9 @@ <menuitem id="menu_config_address_book" parent="menu_sale_config" name="Contacts" sequence="4" groups="base.group_system"/> <menuitem action="base.action_partner_category_form" id="menu_partner_category_form" name="Contact Tags" sequence="1" parent="menu_config_address_book" groups="base.group_no_one"/> <menuitem action="base.action_partner_title_contact" id="menu_partner_title_contact" name="Contact Titles" parent="menu_config_address_book" sequence="3" groups="base.group_no_one"/> + <menuitem id="res_partner_industry_menu" name="Sectors of Activity" action="base.res_partner_industry_action" parent="sales_team.menu_config_address_book" sequence="4" groups="base.group_no_one"/> <menuitem id="menu_localisation" name="Localization" parent="menu_config_address_book" sequence="5" groups="base.group_no_one"/> + <menuitem action="base.action_country" id="menu_country_partner" parent="menu_localisation" sequence="1" groups="base.group_no_one"/> <menuitem action="base.action_country_group" id="menu_country_group" name="Country Group" parent="menu_localisation" sequence="3" groups="base.group_no_one"/> <menuitem action="base.action_country_state" id="menu_country_state_partner" parent="menu_localisation" sequence="2" groups="base.group_no_one"/> diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 8012a1b2a19..011fe65ea8c 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -317,6 +317,8 @@ class ProcurementOrder(models.Model): substract_quantity = location_orderpoints.subtract_procurements_from_orderpoints() for group in location_data['groups']: + if group.get('from_date'): + product_context['from_date'] = group['from_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) if group['to_date']: product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) product_quantity = location_data['products'].with_context(product_context)._product_available() diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 63a9be564aa..54b4a877fd7 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -255,11 +255,11 @@ class Product(models.Model): # TDE FIXME: should probably clean the search methods # to prevent sql injections if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'): - raise UserError('Invalid domain left operand') + raise UserError(_('Invalid domain left operand %s') % field) if operator not in ('<', '>', '=', '!=', '<=', '>='): - raise UserError('Invalid domain operator') + raise UserError(_('Invalid domain operator %s') % operator) if not isinstance(value, (float, int)): - raise UserError('Invalid domain right operand') + raise UserError(_('Invalid domain right operand %s') % value) # TODO: Still optimization possible when searching virtual quantities ids = [] diff --git a/addons/stock/models/stock_incoterms.py b/addons/stock/models/stock_incoterms.py index 0d8a1258fbf..c315e36abe5 100644 --- a/addons/stock/models/stock_incoterms.py +++ b/addons/stock/models/stock_incoterms.py @@ -9,7 +9,7 @@ class Incoterms(models.Model): _description = "Incoterms" name = fields.Char( - 'Name', required=True, + 'Name', required=True, translate=True, help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices.") code = fields.Char( 'Code', size=3, required=True, diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index 7a24f7e6559..f64d2318b2a 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -19,9 +19,6 @@ <div t-field="o.partner_id" t-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": True}'/> </div> - <div t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header"> - t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> - </div> </div> </div> <h2> diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index bbcbafb7b08..f49daf00cf7 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -118,6 +118,7 @@ </field> </record> + <!-- view common to both template and product --> <record id="view_template_property_form" model="ir.ui.view"> <field name="name">product.template.stock.property.form.inherit</field> <field name="model">product.template</field> @@ -133,7 +134,7 @@ <field name="sale_delay" attrs="{'readonly':[('sale_ok','=',False)]}" class="oe_inline" style="vertical-align:baseline"/> days </div> </group> - <group string="Logistics" name="group_lots_and_weight" attrs="{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}"> + <group string="Logistics" name="group_lots_and_weight" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"> <label for="weight"/> <div class="o_row" name="weight"> <field name="weight"/> @@ -260,6 +261,7 @@ </field> </record> + <!-- view used for product.product only --> <record model="ir.ui.view" id="product_form_view_procurement_button"> <field name="name">product.product.procurement</field> <field name="model">product.product</field> @@ -330,6 +332,7 @@ </field> </record> + <!-- view used for product.template only --> <record model="ir.ui.view" id="product_template_form_view_procurement_button"> <field name="name">product.template_procurement</field> <field name="model">product.template</field> @@ -391,6 +394,12 @@ class="oe_stat_button" icon="fa-cogs" groups="stock.group_adv_location"/> </button> + + <!-- change attrs of fields added in view_template_property_form + to restrict the display for templates --> + <xpath expr="//group[@name='group_lots_and_weight']" position="attributes"> + <attribute name="attrs">{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}</attribute> + </xpath> </data> </field> </record> diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index a33039ce7bd..67997c74854 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -86,7 +86,7 @@ <field name="package_id" groups="stock.group_tracking_lot"/> <field name="packaging_type_id" invisible="1"/> <field name="in_date"/> - <field name="inventory_value"/> + <field name="inventory_value" sum="Total"/> <field name="reservation_id" invisible='1'/> <field name="propagated_from_id" invisible='1'/> <field name='company_id' groups="base.group_multi_company"/> diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index aacdd6c7177..6291da2922f 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -38,7 +38,7 @@ class AccountInvoice(models.Model): cacc = accounts['expense'].id if dacc and cacc: price_unit = i_line._get_anglo_saxon_price_unit() - if inv.currency_id.id != company_currency: + if inv.currency_id != company_currency: currency_id = inv.currency_id.id amount_currency = i_line._get_price(company_currency, price_unit) else: @@ -99,6 +99,6 @@ class AccountInvoiceLine(models.Model): def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - if accounts['stock_input']: + if product.categ_id.property_valuation != 'manual_periodic' and accounts['stock_input']: return accounts['stock_input'] return super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/addons/stock_account/wizard/wizard_valuation_history_views.xml b/addons/stock_account/wizard/wizard_valuation_history_views.xml index 48e8162e793..abadf22cd07 100644 --- a/addons/stock_account/wizard/wizard_valuation_history_views.xml +++ b/addons/stock_account/wizard/wizard_valuation_history_views.xml @@ -25,6 +25,7 @@ <field name="view_mode">form</field> <field name="view_id" ref="view_wizard_valuation_history"/> <field name="target">new</field> + <field name="context">{'default_compute_at_date': 0}</field> </record> <menuitem id="stock.menu_quants" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="120" action="action_wizard_stock_valuation_history"/> </odoo> diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 982fd05b1e0..9f56cd01ef4 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -76,6 +76,10 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, { if (event.data.on_success) { event.data.on_success(result); } + }).fail(function (result) { + if (event.data.on_fail) { + event.data.on_fail(result); + } }); }, }, diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index b02376ef6b7..c81479ec1e0 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -182,19 +182,35 @@ var ViewManagerAction = WidgetAction.extend({ }, /** * Sets the scroll position of the widgets's active_view - * @param {int} [scrollTop] the number of pixels to scroll + * @todo: replace this with a generic get/set local state mechanism. + * @see getScrollTop + * + * @override + * @param {integer} [scrollTop] the number of pixels to scroll */ - setScrollTop: function(scrollTop) { - var viewController = this.widget.active_view.controller; + setScrollTop: function (scrollTop) { + var activeView = this.widget.active_view; + var viewController = activeView && activeView.controller; if (viewController) { viewController.setScrollTop(scrollTop); } }, /** - * @return {int} the number of pixels the webclient is scrolled when leaving the action + * Returns the current scrolling offset for the current action. We have to + * ask nicely the question to the active view, because the answer depends + * on the view. + * + * @todo: replace this mechanism with a generic getLocalState and + * getLocalState. Scrolling behaviour is only a part of what we might want + * to restore. + * + * @override + * @returns {integer} the number of pixels the webclient is currently + * scrolled */ - getScrollTop: function() { - var viewController = this.widget.active_view.controller; + getScrollTop: function () { + var activeView = this.widget.active_view; + var viewController = activeView && activeView.controller; return viewController ? viewController.getScrollTop() : 0; }, /** diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js index bc441f7accf..8d760c568b0 100644 --- a/addons/web/static/src/js/chrome/view_manager.js +++ b/addons/web/static/src/js/chrome/view_manager.js @@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { custom_events: { execute_action: function(event) { var data = event.data; - this.do_execute_action(data.action_data, data.model, data.record_id, data.on_closed) + this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed) .then(data.on_success, data.on_fail); }, search: function(event) { @@ -551,13 +551,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, { * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'`` * @param {Object} [action_data.context=null] additional action context, to add to the current context * @param {DataSet} dataset a dataset object used to communicate with the server - * @param {Object} [record_id] the identifier of the object on which the action is to be applied + * @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set) * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ - do_execute_action: function (action_data, model, record_id, on_closed) { + do_execute_action: function (action_data, model, res_ids, on_closed) { var self = this; var result_handler = on_closed || function () {}; var context = new Context(this.env.context, action_data.context || {}); + var record_id = res_ids && res_ids[0]; // response handler var handler = function (action) { @@ -572,10 +573,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, { ); ncontext.add(action_data.context || {}); ncontext.add({active_model: self.env.modelName}); - if (record_id) { + if (res_ids) { ncontext.add({ active_id: record_id, - active_ids: [record_id], + active_ids: res_ids, }); } ncontext.add(action.context || {}); @@ -592,7 +593,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { if (action_data.special) { return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type === "object") { - var args = record_id ? [[record_id]] : [this.env.ids]; + var args = res_ids ? [res_ids] : [this.env.ids]; if (action_data.args) { try { // Warning: quotes and double quotes problem due to json and xml clash @@ -610,7 +611,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), { active_model: this.env.modelName, active_ids: this.env.ids, - active_id: record_id + active_id: record_id, })).then(handler); } }, diff --git a/addons/web/static/src/js/core/mixins.js b/addons/web/static/src/js/core/mixins.js index 9c643597f04..cc99cc1aaa4 100644 --- a/addons/web/static/src/js/core/mixins.js +++ b/addons/web/static/src/js/core/mixins.js @@ -511,6 +511,7 @@ var ServicesMixin = { action: action, options: options, on_success: function (result) { def.resolve(result); }, + on_fail: function (result) { def.reject(result); }, }); return def; }, diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 725a237750d..d09174b2b20 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -348,8 +348,11 @@ var AbstractField = Widget.extend({ * * @private * @param {any} value + * @param {Object} [options] + * @param {boolean} [options.forceChange=false] if true, the change event will be + * triggered even if the new value is the same as the old one */ - _setValue: function (value) { + _setValue: function (value, options) { // we try to avoid doing useless work, if the value given has not // changed. Note that we compare the unparsed values. if (this.lastSetValue === value || (this.value === false && value === '')) { @@ -363,7 +366,7 @@ var AbstractField = Widget.extend({ this._isValid = false; return; } - if (this._isSameValue(value)) { + if (!(options && options.forceChange) && this._isSameValue(value)) { return; } var changes = {}; @@ -371,6 +374,7 @@ var AbstractField = Widget.extend({ this.trigger_up('field_changed', { dataPointID: this.dataPointID, changes: changes, + viewType: this.viewType, }); }, @@ -404,6 +408,9 @@ var AbstractField = Widget.extend({ ev.stopPropagation(); this.trigger_up('navigation_move', {direction: 'next_line'}); break; + case $.ui.keyCode.ESCAPE: + this.trigger_up('navigation_move', {direction: 'cancel'}); + break; case $.ui.keyCode.UP: ev.stopPropagation(); this.trigger_up('navigation_move', {direction: 'up'}); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 49bcccd2640..a5ceba50d3f 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -83,10 +83,10 @@ var DebouncedField = AbstractField.extend({ init: function () { this._super.apply(this, arguments); - // _debouncedStarted is used to detect that the user interacted at least + // _isDirty is used to detect that the user interacted at least // once with the widget, so that we can prevent it from triggering a // field_changed in commitChanges if the user didn't change anything - this._debouncedStarted = false; + this._isDirty = false; if (this.mode === 'edit') { if (this.DEBOUNCE) { this._doDebouncedAction = _.debounce(this._doAction, this.DEBOUNCE); @@ -97,7 +97,7 @@ var DebouncedField = AbstractField.extend({ var self = this; var debouncedFunction = this._doDebouncedAction; this._doDebouncedAction = function () { - self._debouncedStarted = true; + self._isDirty = true; debouncedFunction.apply(self, arguments); }; } @@ -115,7 +115,7 @@ var DebouncedField = AbstractField.extend({ * @override */ commitChanges: function () { - if (this._debouncedStarted && this.mode === 'edit') { + if (this._isDirty && this.mode === 'edit') { this._doAction(); } }, @@ -132,7 +132,13 @@ var DebouncedField = AbstractField.extend({ * @private */ _doAction: function () { - this._setValue(this._getValue()); + // as _doAction may be debounced, it may happen that it is called after + // the widget has been destroyed, and in this case, we don't want it to + // do anything (commitChanges ensures that if it has local changes, they + // are triggered up before the widget is destroyed, if necessary). + if (!this.isDestroyed()) { + this._setValue(this._getValue()); + } }, /** * Should return the current value of the field, in the DOM (for example, @@ -159,6 +165,7 @@ var InputField = DebouncedField.extend({ */ init: function () { this._super.apply(this, arguments); + this.nodeOptions.isPassword = 'password' in this.attrs; if (this.mode === 'edit') { this.tagName = 'input'; } @@ -216,8 +223,11 @@ var InputField = DebouncedField.extend({ this.$input = $input || $("<input/>"); this.$input.addClass('o_input'); this.$input.attr({ - type: 'text', + type: this.nodeOptions.isPassword ? 'password' : 'text', placeholder: this.attrs.placeholder || "", + autocomplete: this.nodeOptions.isPassword ? + 'new-password' : + this.attrs.autocomplete, }); this.$input.val(this._formatValue(this.value)); return this.$input; @@ -240,7 +250,7 @@ var InputField = DebouncedField.extend({ * @private */ _renderReadonly: function () { - this.$el.html(this._formatValue(this.value)); + this.$el.text(this._formatValue(this.value)); }, //-------------------------------------------------------------------------- @@ -524,6 +534,13 @@ var FieldMonetary = InputField.extend({ } }, /** + * @override + * @private + */ + _renderReadonly: function () { + this.$el.html(this._formatValue(this.value)); + }, + /** * Re-gets the currency as its value may have changed. * @see FieldMonetary.resetOnAnyFieldChange * @@ -541,7 +558,7 @@ var FieldMonetary = InputField.extend({ * @private */ _setCurrency: function () { - var currencyField = this.attrs.currency_field || this.field.currency_field || 'currency_id'; + var currencyField = this.nodeOptions.currency_field || this.field.currency_field || 'currency_id'; var currencyID = this.record.data[currencyField] && this.record.data[currencyField].res_id; this.currency = session.get_currency(currencyID); }, @@ -885,8 +902,8 @@ var FieldPhone = FieldEmail.extend({ _renderReadonly: function () { this._super(); if (this._canCall()) { - var text = this.$el.text(); // Split phone number into two to prevent Skype app from finding it + var text = this.$el.text(); var part1 = _.escape(text.substr(0, text.length/2)); var part2 = _.escape(text.substr(text.length/2)); this.$el.html(part1 + "­" + part2); @@ -1543,7 +1560,7 @@ var FieldPercentPie = AbstractField.extend({ this.$leftMask.css({transform: leftDeg, msTransform: leftDeg, mozTransform: leftDeg, webkitTransform: leftDeg}); this.$rightMask.css({transform: rightDeg, msTransform: rightDeg, mozTransform: rightDeg, webkitTransform: rightDeg}); - this.$pieValue.html(Math.round(value) + '%'); + this.$pieValue.text(Math.round(value) + '%'); }, }); @@ -1684,9 +1701,9 @@ var FieldProgressBar = AbstractField.extend({ if (!this.write_mode) { if (max_value !== 100) { - this.$('.o_progressbar_value').html(utils.human_number(value) + " / " + utils.human_number(max_value)); + this.$('.o_progressbar_value').text(utils.human_number(value) + " / " + utils.human_number(max_value)); } else { - this.$('.o_progressbar_value').html(utils.human_number(value) + "%"); + this.$('.o_progressbar_value').text(utils.human_number(value) + "%"); } } else if (isNaN(v)) { this.$('.o_progressbar_value').val(this.edit_max_value ? max_value : value); diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index ed3014d9bb6..9a6734b7baf 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -52,10 +52,23 @@ function formatBoolean(value) { * an empty string. * * @param {string|false} value + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.escape=false] if true, escapes the formatted value + * @param {boolean} [options.isPassword=false] if true, returns '********' + * instead of the formatted value * @returns {string} */ -function formatChar(value) { - return typeof value === 'string' ? value : ''; +function formatChar(value, field, options) { + value = typeof value === 'string' ? value : ''; + if (options && options.isPassword) { + return _.str.repeat('*', value ? value.length : 0); + } + if (options && options.escape) { + value = _.escape(value); + } + return value; } /** @@ -169,9 +182,16 @@ function formatFloatTime(value) { * return an empty string. * * @param {integer|false} value + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.isPassword=false] if true, returns '********' * @returns {string} */ -function formatInteger(value) { +function formatInteger(value, field, options) { + if (options && options.isPassword) { + return _.str.repeat('*', String(value).length); + } if (!value && value !== 0) { // previously, it returned 'false'. I don't know why. But for the Pivot // view, I want to display the concept of 'no value' with an empty @@ -189,10 +209,18 @@ function formatInteger(value) { * case, we assume that it is a record from a BasicModel. * * @param {Array|Object|false} value + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.escape=false] if true, escapes the formatted value * @returns {string} */ -function formatMany2one(value) { - return value && (_.isArray(value) ? value[1] : value.data.display_name) || ''; +function formatMany2one(value, field, options) { + value = value && (_.isArray(value) ? value[1] : value.data.display_name) || ''; + if (options && options.escape) { + value = _.escape(value); + } + return value; } /** @@ -270,14 +298,27 @@ function formatMonetary(value, field, options) { } } -function formatSelection(value, field) { - if (!value) { - return ''; - } +/** + * Returns a string representing the value of the selection. + * + * @param {string|false} value + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.escape=false] if true, escapes the formatted value + */ +function formatSelection(value, field, options) { var val = _.find(field.selection, function (option) { return option[0] === value; }); - return val[1]; + if (!val) { + return ''; + } + value = val[1]; + if (options && options.escape) { + value = _.escape(value); + } + return value; } //////////////////////////////////////////////////////////////////////////////// @@ -318,7 +359,7 @@ function parseDate(value, field, options) { } if (date.year() >= 1900) { date.toJSON = function () { - return this.format('YYYY-MM-DD'); + return this.clone().locale('en').format('YYYY-MM-DD'); }; return date; } @@ -365,7 +406,7 @@ function parseDateTime(value, field, options) { } if (datetime.year() >= 1900) { datetime.toJSON = function () { - return this.format('YYYY-MM-DD HH:mm:ss'); + return this.clone().locale('en').format('YYYY-MM-DD HH:mm:ss'); }; return datetime; } @@ -374,7 +415,10 @@ function parseDateTime(value, field, options) { } function parseFloat(value) { - value = value.replace(new RegExp(core._t.database.parameters.thousands_sep, "g"), ''); + if (core._t.database.parameters.thousands_sep) { + var escapedSep = _.str.escapeRegExp(core._t.database.parameters.thousands_sep); + value = value.replace(new RegExp(escapedSep, 'g'), ''); + } value = value.replace(core._t.database.parameters.decimal_point, '.'); var parsed = Number(value); if (isNaN(parsed)) { diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 10d72a1398d..a5f9b443ae0 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -88,6 +88,7 @@ var FieldMany2One = AbstractField.extend({ 'click .o_external_button': '_onExternalButtonClick', 'click': '_onClick', }), + AUTOCOMPLETE_DELAY: 200, init: function () { this._super.apply(this, arguments); @@ -147,6 +148,11 @@ var FieldMany2One = AbstractField.extend({ }); }, select: function (event, ui) { + // we do not want the select event to trigger any additional + // effect, such as navigating to another field. + event.stopImmediatePropagation(); + event.preventDefault(); + var item = ui.item; self.floating = false; if (item.id) { @@ -159,10 +165,17 @@ var FieldMany2One = AbstractField.extend({ focus: function (event) { event.preventDefault(); // don't automatically select values on focus }, + close: function (event) { + // it is necessary to prevent ESC key from propagating to field + // root, to prevent unwanted discard operations. + if (event.which === $.ui.keyCode.ESCAPE) { + event.stopPropagation(); + } + }, autoFocus: true, html: true, minLength: 0, - delay: 200, + delay: this.AUTOCOMPLETE_DELAY, }); this.$input.autocomplete("option", "position", { my : "left top", at: "left bottom" }); this.autocomplete_bound = true; @@ -250,6 +263,7 @@ var FieldMany2One = AbstractField.extend({ */ _reset: function () { this._super.apply(this, arguments); + this.floating = false; this.m2o_value = field_utils.format.many2one(this.value); }, /** @@ -427,6 +441,7 @@ var FieldMany2One = AbstractField.extend({ view_id: view_id, readonly: !self.can_write, on_saved: function () { + self._setValue(self.value.data, {forceChange: true}); self.trigger_up('reload', {db_id: self.value.id}); }, }).open(); @@ -462,6 +477,14 @@ var FieldMany2One = AbstractField.extend({ } }, /** + * @override + * @private + */ + _onKeydown: function () { + this.floating = false; + this._super.apply(this, arguments); + }, + /** * Stops the left/right navigation move event if the cursor is not at the * start/end of the input element. Stops any navigation move event if the * user is selecting text. @@ -531,6 +554,7 @@ var FieldX2Many = AbstractField.extend({ tagName: 'div', custom_events: _.extend({}, AbstractField.prototype.custom_events, { add_record: '_onAddRecord', + discard_changes: '_onDiscardChanges', edit_line: '_onEditLine', field_changed: '_onFieldChanged', kanban_record_delete: '_onDeleteRecord', @@ -740,7 +764,7 @@ var FieldX2Many = AbstractField.extend({ this.renderer.commitChanges(recordID).then(function () { // TODO wrong as no mutex protection var fieldNames = self.renderer.canBeSaved(recordID); if (fieldNames.length) { - self.trigger_up('discard_x2m_changes', { + self.trigger_up('discard_changes', { recordID: recordID, onSuccess: def.resolve.bind(def), onFailure: def.reject.bind(def), @@ -783,6 +807,18 @@ var FieldX2Many = AbstractField.extend({ }); }, /** + * When the discard_change event go through this field, we can just decorate + * the data with the name of the field. The origin field ignore this + * information (it is a subfield in a o2m), and the controller will need to + * know which field needs to be handled. + * + * @private + * @param {OdooEvent} ev + */ + _onDiscardChanges: function (ev) { + ev.data.fieldName = this.name; + }, + /** * Called when the renderer asks to edit a line, in that case simply tells * him back to toggle the mode of this row. * @@ -837,15 +873,30 @@ var FieldX2Many = AbstractField.extend({ * changes; if the row could be saved, we make the row readonly. Otherwise, * we trigger a new event for the view to tell it to discard the changes * made to that row. + * Note that we do that in the controller mutex to ensure that the check on + * the row (whether or not it can be saved) is done once all potential + * onchange RPCs are done (those RPCs being executed in the same mutex). + * This particular handling is done in this handler, instead of in the + * _saveLine function directly, because _saveLine is also called from + * the controller (via commitChanges), and in this case, it is already + * executed in the mutex. * * @private * @param {OdooEvent} ev + * @param {string} ev.recordID + * @param {function} ev.onSuccess success callback (see '_saveLine') + * @param {function} ev.onFailure fail callback (see '_saveLine') */ _onSaveLine: function (ev) { + var self = this; ev.stopPropagation(); - this._saveLine(ev.data.recordID) - .done(ev.data.onSuccess) - .fail(ev.data.onFailure); + this.trigger_up('mutexify', { + action: function () { + return self._saveLine(ev.data.recordID) + .done(ev.data.onSuccess) + .fail(ev.data.onFailure); + }, + }); }, /** * Adds field name information to the event, so that the view upstream is @@ -863,6 +914,16 @@ var FieldOne2Many = FieldX2Many.extend({ className: 'o_field_one2many', supportedFieldTypes: ['one2many'], + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + + // boolean used to prevent concurrent record creation + this.creatingRecord = false; + }, + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -880,6 +941,7 @@ var FieldOne2Many = FieldX2Many.extend({ var index = self.editable === 'top' ? 0 : self.value.data.length - 1; var newID = self.value.data[index].id; self.renderer.editRecord(newID); + self.creatingRecord = false; } } }); @@ -933,10 +995,13 @@ var FieldOne2Many = FieldX2Many.extend({ ev.stopPropagation(); if (this.editable) { - this._setValue({ - operation: 'CREATE', - position: this.editable, - }); + if (!this.creatingRecord) { + this.creatingRecord = true; + this._setValue({ + operation: 'CREATE', + position: this.editable, + }); + } } else { var self = this; this._openFormDialog({ @@ -962,7 +1027,7 @@ var FieldOne2Many = FieldX2Many.extend({ this._openFormDialog({ id: ev.data.id, - on_saved: this._setValue.bind(this, { operation: 'NOOP' }), + on_saved: this._setValue.bind(this, { operation: 'NOOP' }, {}), readonly: this.mode === 'readonly', }); }, @@ -1026,11 +1091,15 @@ var FieldMany2Many = FieldX2Many.extend({ * @param {OdooEvent} ev */ _onOpenRecord: function (ev) { + var self = this; _.extend(ev.data, { context: this.record.getContext(this.recordParams), domain: this.record.getDomain(this.recordParams), fields_view: this.attrs.views && this.attrs.views.form, - on_saved: this.trigger_up.bind(this, 'reload', {db_id: ev.data.id}), + on_saved: function () { + self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true}); + self.trigger_up('reload', {db_id: ev.data.id}); + }, readonly: this.mode === 'readonly', string: this.string, }); @@ -1671,7 +1740,7 @@ var FieldSelection = AbstractField.extend({ }, //-------------------------------------------------------------------------- - // Private + // Public //-------------------------------------------------------------------------- /** @@ -1681,6 +1750,12 @@ var FieldSelection = AbstractField.extend({ getFocusableElement: function () { return this.$el.is('select') ? this.$el : $(); }, + /** + * @override + */ + isSet: function () { + return this.value !== false; + }, //-------------------------------------------------------------------------- // Private @@ -1710,7 +1785,7 @@ var FieldSelection = AbstractField.extend({ * @private */ _renderReadonly: function () { - this.$el.empty().html(this._formatValue(this.value)); + this.$el.empty().text(this._formatValue(this.value)); }, //-------------------------------------------------------------------------- @@ -1779,14 +1854,17 @@ var FieldRadio = FieldSelection.extend({ * @override */ _renderEdit: function () { - this.$("input").prop("checked", false); - var key; + var value; if (this.field.type === 'many2one') { - key = this.value && this.value.data.id; + value = this.value && this.value.data.id; } else { - key = this.value; + value = this.value; } - this.$('input[data-value="' + key + '"]').prop('checked', true); + var index = _.findIndex(this.values, function (option) { + return option[0] === value; + }); + this.$("input").prop("checked", false); + this.$('input[data-index="' + index + '"]').prop('checked', true); }, //-------------------------------------------------------------------------- @@ -1798,14 +1876,12 @@ var FieldRadio = FieldSelection.extend({ * @param {MouseEvent} event */ _onInputClick: function (event) { - var res_id = $(event.target).data('value'); + var index = $(event.target).data('index'); + var value = this.values[index]; if (this.field.type === 'many2one') { - var value = _.find(this.values, function (val) { - return val[0] === res_id; - }); - this._setValue({id: res_id, display_name: value[1]}); + this._setValue({id: value[0], display_name: value[1]}); } else { - this._setValue(res_id); + this._setValue(value[0]); } }, }); diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 1e0187b348e..53e5be8ff6e 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -18,6 +18,8 @@ var _t = core._t; var BasicController = AbstractController.extend(FieldManagerMixin, { custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { + discard_changes: '_onDiscardChanges', + mutexify: '_onMutexify', reload: '_onReload', sidebar_data_asked: '_onSidebarDataAsked', translate: '_onTranslate', @@ -247,9 +249,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { var self = this; var def = $.Deferred(); var reload = function () { - if (!self.isDestroyed()) { - self.reload(); - } + return self.isDestroyed() ? $.when() : self.reload(); }; record = record || this.model.get(this.handle); var recordID = record.data.id; @@ -258,13 +258,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { context: record.getContext({additionalContext: attrs.context}), }), model: record.model, - record_id: recordID, + res_ids: [recordID], on_closed: function (reason) { if (!_.isObject(reason)) { - reload(); + reload(reason); } }, - on_fail: reload, + on_fail: function (reason) { + reload().always(function() { + def.reject(reason); + }) + }, on_success: def.resolve.bind(def), }); return this.alive(def); @@ -306,6 +310,26 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { } }, /** + * Disables buttons so that they can't be clicked anymore. + * + * @private + */ + _disableButtons: function () { + if (this.$buttons) { + this.$buttons.find('button').attr('disabled', true); + } + }, + /** + * Enables buttons so they can be clicked again. + * + * @private + */ + _enableButtons: function () { + if (this.$buttons) { + this.$buttons.find('button').removeAttr('disabled'); + } + }, + /** * Returns the new sidebar env * * @private @@ -385,18 +409,19 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // Note: it is the model's job to do nothing if there is nothing to save if (this.canBeSaved(recordID)) { var self = this; - var def = this.model.save(recordID, { // Save then leave edit mode + var saveDef = this.model.save(recordID, { // Save then leave edit mode reload: options.reload, savePoint: options.savePoint, }); if (!options.stayInEdit) { - def = def.then(function (fieldNames) { - return self._confirmSave(recordID).then(function () { + saveDef = saveDef.then(function (fieldNames) { + var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly'); + return def.then(function () { return fieldNames; }); }); } - return def; + return saveDef; } else { return $.Deferred().reject(); // Cannot be saved } @@ -457,6 +482,31 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { //-------------------------------------------------------------------------- /** + * Called when a list element asks to discard the changes made to one of + * its rows. It can happen with a x2many (if we are in a form view) or with + * a list view. + * + * @private + * @param {OdooEvent} ev + */ + _onDiscardChanges: function (ev) { + var self = this; + ev.stopPropagation(); + var recordID = ev.data.recordID; + this.discardChanges(recordID) + .done(function () { + if (self.model.isNew(recordID)) { + self._abandonRecord(recordID); + } + // TODO this will tell the renderer to rerender the widget that + // asked for the discard but will unfortunately lose the click + // made on another row if any + self._confirmChange(self.handle, [ev.data.fieldName], ev) + .always(ev.data.onSuccess); + }) + .fail(ev.data.onFailure); + }, + /** * Forces to save directly the changes if the controller is in readonly, * because in that case the changes come from widgets that are editable even * in readonly (e.g. Priority). @@ -471,6 +521,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { FieldManagerMixin._onFieldChanged.apply(this, arguments); }, /** + * @private + * @param {OdooEvent} ev + * @param {function} ev.data.action the function to execute in the mutex + */ + _onMutexify: function (ev) { + this.mutex.exec(ev.data.action); + }, + /** * When a reload event triggers up, we need to reload the full view. * For example, after a form view dialog saved some data. * diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 5e5b36472f5..742e59a61c5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -27,7 +27,6 @@ odoo.define('web.BasicModel', function (require) { * fieldsInfo: {Object}, * getContext: {function}, * getDomain: {function}, - * getEvalContext: {function}, * getFieldNames: {function}, * groupedBy: {string[]}, * id: {integer}, @@ -228,7 +227,7 @@ var BasicModel = AbstractModel.extend({ options = options || {}; var element = this.localData[id]; var isNew = this.isNew(id); - var rollback = options.rollback || isNew; + var rollback = 'rollback' in options ? options.rollback : isNew; this._visitChildren(element, function (elem) { if (rollback && elem._savePoint) { if (elem._savePoint instanceof Array) { @@ -284,8 +283,6 @@ var BasicModel = AbstractModel.extend({ * @param {boolean} [options.env=false] if true, will only return res_id * (if record) or res_ids (if list) * @param {boolean} [options.raw=false] if true, will not follow relations - * @param {boolean} [options.noUnsetNumeric=false] if true, will set numeric - * values to 0 if not set * @returns {Object} */ get: function (id, options) { @@ -308,7 +305,6 @@ var BasicModel = AbstractModel.extend({ return env; } - if (element.type === 'record') { var data = _.extend({}, element.data, element._changes); @@ -321,13 +317,6 @@ var BasicModel = AbstractModel.extend({ if (!field) { continue; } - if (options.noUnsetNumeric) { - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - data[fieldName] = data[fieldName] || 0; - } - } // get relational datapoint if (field.type === 'many2one') { @@ -356,11 +345,11 @@ var BasicModel = AbstractModel.extend({ count: element.count, data: data, domain: element.domain.slice(0), + evalModifiers: element.evalModifiers, fields: element.fields, fieldsInfo: element.fieldsInfo, getContext: element.getContext, getDomain: element.getDomain, - getEvalContext: element.getEvalContext, getFieldNames: element.getFieldNames, id: element.id, limit: element.limit, @@ -410,7 +399,6 @@ var BasicModel = AbstractModel.extend({ fields: element.fields, getContext: element.getContext, getDomain: element.getDomain, - getEvalContext: element.getEvalContext, getFieldNames: element.getFieldNames, groupedBy: element.groupedBy, id: element.id, @@ -615,10 +603,12 @@ var BasicModel = AbstractModel.extend({ * * @param {string} record_id * @param {Object} changes a map field => new value + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {string[]} list of changed fields */ - notifyChanges: function (record_id, changes) { - return this.mutex.exec(this._applyChange.bind(this, record_id, changes)); + notifyChanges: function (record_id, changes, viewType) { + return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType)); }, /** * Reload all data for a given resource @@ -644,7 +634,7 @@ var BasicModel = AbstractModel.extend({ return this._makeDefaultRecord(element.model, params); } if (!options.keepChanges) { - this.discardChanges(id); + this.discardChanges(id, {rollback: false}); } } @@ -788,7 +778,7 @@ var BasicModel = AbstractModel.extend({ _.extend(record.data, _changes); def.resolve(changedFields); } - }); + }).fail(def.reject.bind(def)); } else { def.resolve(changedFields); } @@ -902,9 +892,11 @@ var BasicModel = AbstractModel.extend({ * * @param {string} recordID * @param {Object} changes + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} */ - _applyChange: function (recordID, changes) { + _applyChange: function (recordID, changes, viewType) { var self = this; var record = this.localData[recordID]; var field; @@ -916,7 +908,7 @@ var BasicModel = AbstractModel.extend({ for (var fieldName in changes) { field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { - defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName])); + defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType)); } else if (field.type === 'many2one') { defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName])); } else { @@ -937,7 +929,7 @@ var BasicModel = AbstractModel.extend({ } var onchangeDef; if (onChangeFields.length) { - onchangeDef = self._performOnChange(record, onChangeFields).then(function (result) { + onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) { delete record._warning; return _.keys(changes).concat(Object.keys(result && result.value || {})); }); @@ -976,6 +968,17 @@ var BasicModel = AbstractModel.extend({ record._changes[fieldName] = false; return $.when(); } + + // here, we check that the many2one really changed. If the res_id is the + // same, we do not need to do any extra work. It can happen when the + // user edited a manyone (with the small form view button) with an + // onchange. In that case, the onchange is triggered, but the actual + // value did not change. + var relatedID = (record._changes && record._changes[fieldName]) || record.data[fieldName]; + var relatedRecord = this.localData[relatedID]; + if (relatedRecord && (data.id === this.localData[relatedID].res_id)) { + return $.when(); + } var rel_data = _.pick(data, 'id', 'display_name'); var def; if (rel_data.display_name === undefined) { @@ -1093,14 +1096,8 @@ var BasicModel = AbstractModel.extend({ }); defs.push(def); } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(val); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(val); } else { - record._changes[name] = val; + record._changes[name] = self._parseServerValue(field, val); } }); return $.when.apply($, defs); @@ -1115,13 +1112,23 @@ var BasicModel = AbstractModel.extend({ * @param {string} fieldName * @param {Object} command A command object. It should have a 'operation' * key. For example, it looks like {operation: ADD, id: 'partner_1'} + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} */ - _applyX2ManyChange: function (record, fieldName, command) { + _applyX2ManyChange: function (record, fieldName, command, viewType) { + if (command.operation === 'TRIGGER_ONCHANGE') { + // the purpose of this operation is to trigger an onchange RPC, so + // there is no need to apply any change on the record (the changes + // have probably been already applied and saved, usecase: many2many + // edition in a dialog) + return $.when(); + } + var self = this; var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; var field = record.fields[fieldName]; - var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var rec; var defs = []; @@ -1144,6 +1151,7 @@ var BasicModel = AbstractModel.extend({ newRecord.fieldsInfo = list.fieldsInfo; newRecord.viewType = list.viewType; list._changes.push(newRecord.id); + this._sortList(list); break; case 'ADD_M2M': // force to use link command instead of create command @@ -1163,6 +1171,7 @@ var BasicModel = AbstractModel.extend({ }); list_records[d.id] = rec; list._changes.push(rec.id); + self._sortList(list); }); // read list's records as we only have their ids and optionally their display_name // (we can't use function readUngroupedList because those records are only in the @@ -1243,22 +1252,65 @@ var BasicModel = AbstractModel.extend({ * @see _performOnChange * * @param {Object} record resource object of type 'record' - * @returns {Object} an onchange spec + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record + * @returns {Object|false} an onchange spec, or false if no onchange should + * be applied */ - _buildOnchangeSpecs: function (record) { - // TODO: replace this function by some generic tree function in utils + _buildOnchangeSpecs: function (record, viewType) { + var hasOnchange = false; var specs = {}; - _.each(record.getFieldNames(), function (name) { + var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; + + _.each(Object.keys(fieldsInfo), function (name) { var field = record.fields[name]; - var fieldInfo = record.fieldsInfo[record.viewType][name]; + var fieldInfo = fieldsInfo[name]; specs[name] = (field.onChange) || ""; + if (field.onChange) { + hasOnchange = true; + } _.each(fieldInfo.views, function (view) { _.each(view.fieldsInfo[view.type], function (field, subname) { - specs[name + '.' + subname] = (view.fields[subname].onChange) || ""; + var onChange = view.fields[subname].onChange; + specs[name + '.' + subname] = onChange || ""; + if (onChange) { + hasOnchange = true; + } }); }); }); - return specs; + return hasOnchange ? specs : false; + }, + /** + * Evaluate modifiers + * + * @private + * @param {Object} element a valid element object, which will serve as eval + * context. + * @param {Object} modifiers + * @returns {Object} + */ + _evalModifiers: function (element, modifiers) { + var result = {}; + var self = this; + var evalContext; + function evalModifier (mod) { + if (mod === undefined || mod === false || mod === true) { + return !!mod; + } + evalContext = evalContext || self._getEvalContext(element); + return new Domain(mod, evalContext).compute(evalContext); + } + if ('invisible' in modifiers) { + result.invisible = evalModifier(modifiers.invisible); + } + if ('readonly' in modifiers) { + result.readonly = evalModifier(modifiers.readonly); + } + if ('required' in modifiers) { + result.required = evalModifier(modifiers.required); + } + return result; }, /** * Fetch all name_gets for the many2ones in a group @@ -1899,8 +1951,8 @@ var BasicModel = AbstractModel.extend({ // generate update commands for records that have been // updated (it may happen with editable lists) _.each(relData, function (relRecord) { - if (!_.isEmpty(relRecord._changes)) { - var changes = self._generateChanges(relRecord); + var changes = self._generateChanges(relRecord); + if (!_.isEmpty(changes)) { var command = x2ManyCommands.update(relRecord.res_id, changes); commands[fieldName].push(command); } @@ -1918,8 +1970,8 @@ var BasicModel = AbstractModel.extend({ if (_.contains(keptIds, relIds[i])) { // this is an id that already existed relRecord = _.findWhere(relData, {res_id: relIds[i]}); - if (!_.isEmpty(relRecord._changes)) { - changes = this._generateChanges(relRecord); + changes = this._generateChanges(relRecord); + if (!_.isEmpty(changes)) { command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { @@ -2046,19 +2098,24 @@ var BasicModel = AbstractModel.extend({ * @returns {Object} */ _getEvalContext: function (element) { - var evalContext = this.get(element.id, {raw: true, noUnsetNumeric: true}).data; - evalContext.active_model = element.model; - evalContext.id = evalContext.id || false; - evalContext.active_id = evalContext.id; - evalContext.active_ids = evalContext.id ? [evalContext.id] : []; + var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {}; + if (element.parentID) { - var parent = this.get(element.parentID, {raw: true}); - if (parent.type === 'list' && this.localData[element.parentID].parentID) { - parent = this.get(this.localData[element.parentID].parentID, {raw: true}); + var parent = this.localData[element.parentID]; + if (parent.type === 'list' && parent.parentID) { + parent = this.localData[parent.parentID]; + } + if (parent.type === 'record') { + evalContext.parent = this._getRecordEvalContext(parent); } - _.extend(evalContext, {parent: parent.data}); } - return _.extend({}, session.user_context, element.context, evalContext); + return _.extend({ + active_id: evalContext.id || false, + active_ids: evalContext.id ? [evalContext.id] : [], + active_model: element.model, + current_date: moment().format('YYYY-MM-DD'), + id: evalContext.id || false, + }, session.user_context, element.context, evalContext); }, /** * Returns the list of field names of the given element according to its @@ -2072,6 +2129,67 @@ var BasicModel = AbstractModel.extend({ return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {}); }, /** + * Evaluate the record evaluation context. This method is supposed to be + * called by _getEvalContext. It basically only generates a dictionary of + * current values for the record, with commands for x2manys fields. + * + * @param {Object} record an element of type 'record' + * @returns Object + */ + _getRecordEvalContext: function (record) { + var self = this; + var relDataPoint; + var context = _.extend({}, record.data, record._changes); + for (var fieldName in context) { + var field = record.fields[fieldName]; + if (context[fieldName] === null) { + context[fieldName] = false; + } + if (!field) { + continue; + } + if (field.type === 'float' || + field.type === 'integer' || + field.type === 'monetary') { + context[fieldName] = context[fieldName] || 0; + continue; + } + if (field.type === 'date' || field.type === 'datetime') { + if (context[fieldName]) { + context[fieldName] = JSON.parse(JSON.stringify(context[fieldName])); + } + continue; + } + if (field.type === 'many2one') { + relDataPoint = this.localData[context[fieldName]]; + context[fieldName] = relDataPoint ? relDataPoint.res_id : false; + continue; + } + if (field.type === 'one2many' || field.type === 'many2many') { + relDataPoint = this.localData[context[fieldName]]; + var relData = relDataPoint._changes || relDataPoint.data; + var ids = _.map(relData, function (id) { + return self.localData[id].res_id; + }); + + ids.toJSON = function () { + return _.map(relData, function (id) { + var resID = self.localData[id].res_id; + if (typeof resID === 'string') { + var changes = self._generateChanges(self.localData[id]); + return x2ManyCommands.create(changes); + } else { + return x2ManyCommands.link_to(resID); + } + }); + }; + context[fieldName] = ids; + } + + } + return context; + }, + /** * Returns true iff value is considered to be set for the given field's type. * * @private @@ -2104,11 +2222,17 @@ var BasicModel = AbstractModel.extend({ _isX2ManyValid: function (id) { var self = this; var isValid = true; - var element = this.get(id, {raw: true}); - _.each(element.getFieldNames(), function (fieldName) { - var field = element.fields[fieldName]; - _.each(element.data, function (rec) { - if (field.required && !self._isFieldSet(rec.data[fieldName], field.type)) { + var element = this.localData[id]; + _.each(element._changes || element.data, function (recordID) { + var recordData = self.get(recordID, {raw: true}).data; + var record = self.localData[recordID]; + _.each(element.getFieldNames(), function (fieldName) { + var field = element.fields[fieldName]; + var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; + var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); + var modifiers = self._evalModifiers(record, rawModifiers); + var required = 'required' in modifiers ? modifiers.required : field.required; + if (required && !self._isFieldSet(recordData[fieldName], field.type)) { isValid = false; } }); @@ -2196,9 +2320,9 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }; + dataPoint.evalModifiers = this._evalModifiers.bind(this, dataPoint); dataPoint.getContext = this._getContext.bind(this, dataPoint); dataPoint.getDomain = this._getDomain.bind(this, dataPoint); - dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint); dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint); this.localData[dataPoint.id] = dataPoint; @@ -2300,60 +2424,48 @@ var BasicModel = AbstractModel.extend({ record._changes[name] = x2manyList.id; var many2ones = {}; var r; + var isCommandList = result[name].length && _.isArray(result[name][0]); + if (!isCommandList) { + result[name] = [[6, false, result[name]]]; + } _.each(result[name], function (value) { - if (_.isArray(value)) { - // value is a command - if (value[0] === 0) { - // CREATE - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fieldsInfo: fieldsInfo, - fields: fields, - viewType: viewType, - }); - x2manyList._changes = x2manyList._changes || []; - x2manyList._changes.push(r.id); - - // this is necessary so the fields are initialized - _.each(r.getFieldNames(), function (fieldName) { - r.data[fieldName] = null; - }); - - r._changes = _.defaults(value[2], r.data); - for (var name in r._changes) { - var isFieldInView = name in r.fields; - if (isFieldInView && r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]} - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } - } - } - if (value[0] === 6) { - // REPLACE_WITH - x2manyList.res_ids = value[2]; - x2manyList.count = x2manyList.res_ids.length; - defs.push(self._readUngroupedList(x2manyList)); - } - } else { - // value is an id + // value is a command + if (value[0] === 0) { + // CREATE r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, fieldsInfo: fieldsInfo, fields: fields, - res_id: value, viewType: viewType, }); - if (!x2manyList._changes) { - x2manyList._changes = []; - } + x2manyList._changes = x2manyList._changes || []; x2manyList._changes.push(r.id); + + // this is necessary so the fields are initialized + _.each(r.getFieldNames(), function (fieldName) { + r.data[fieldName] = null; + }); + + r._changes = _.defaults(value[2], r.data); + for (var name in r._changes) { + var isFieldInView = name in r.fields; + if (isFieldInView && r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + x2manyList.res_ids = value[2]; + x2manyList.count = x2manyList.res_ids.length; + defs.push(self._readUngroupedList(x2manyList)); } }); @@ -2361,35 +2473,17 @@ var BasicModel = AbstractModel.extend({ _.each(_.keys(many2ones), function (name) { defs.push(self._fetchNameGets(x2manyList, name)); }); - } else if (field.type === 'date') { - // process date: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(result[name], field, {isUTC: true}); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(result[name], field, {isUTC: true}); } else { - record._changes[name] = result[name]; + record._changes[name] = self._parseServerValue(field, result[name]); } }); return $.when.apply($, defs) .then(function () { - var shouldApplyOnchange = false; - var field; - for (var field_name in record.data) { - field = record.fields[field_name]; - if (field.onChange) { - shouldApplyOnchange = true; + return self._performOnChange(record, fields_key).then(function () { + if (record._warning) { + return $.Deferred().reject(); } - } - if (shouldApplyOnchange) { - return self._performOnChange(record, fields_key).then(function () { - if (record._warning) { - return $.Deferred().reject(); - } - }); - } else { - return $.when(); - } + }); }) .then(function () { return self._fetchRelationalData(record); @@ -2438,27 +2532,52 @@ var BasicModel = AbstractModel.extend({ // no value for the many2one record[fieldName] = false; } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record[fieldName] = fieldUtils.parse.date(val, field, {isUTC: true}); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record[fieldName] = fieldUtils.parse.datetime(val, field, {isUTC: true}); + } else { + record[fieldName] = self._parseServerValue(field, val); } }); }, /** + * Processes date(time) and selection field values sent by the server. + * Converts data(time) values to moment instances. + * Converts false values of selection fields to 0 if 0 is a valid key, + * because the server doesn't make a distinction between false and 0, and + * always sends false when value is 0. + * + * @param {Object} field the field description + * @param {*} value + * @returns {*} the processed value + */ + _parseServerValue: function (field, value) { + if (field.type === 'date' || field.type === 'datetime') { + // process date(time): convert into a moment instance + value = fieldUtils.parse[field.type](value, field, {isUTC: true}); + } else if (field.type === 'selection' && value === false) { + // process selection: convert false to 0, if 0 is a valid key + var hasKey0 = _.find(field.selection, function (option) { + return option[0] === 0; + }); + value = hasKey0 ? 0 : value; + } + return value; + }, + /** * This method is quite important: it is supposed to perform the /onchange * rpc and apply the result. * * @param {Object} record * @param {string[]} fields changed fields + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} The returned deferred can fail, in which case the * fail value will be the warning message received from the server */ - _performOnChange: function (record, fields) { + _performOnChange: function (record, fields, viewType) { var self = this; - var onchange_spec = this._buildOnchangeSpecs(record); + var onchangeSpec = this._buildOnchangeSpecs(record, viewType); + if (!onchangeSpec) { + return $.when(); + } var idList = record.data.id ? [record.data.id] : []; var options = { full: true, @@ -2474,7 +2593,7 @@ var BasicModel = AbstractModel.extend({ return self._rpc({ model: record.model, method: 'onchange', - args: [idList, currentData, fields, onchange_spec, context], + args: [idList, currentData, fields, onchangeSpec, context], }) .then(function (result) { if (!record._changes) { @@ -2580,7 +2699,7 @@ var BasicModel = AbstractModel.extend({ var choice = _.find(list.fields[rawGroupBy].selection, function (c) { return c[0] === value; }); - value = choice[1]; + value = choice ? choice[1] : false; } var newGroup = self._makeDataPoint({ modelName: list.model, @@ -2595,6 +2714,7 @@ var BasicModel = AbstractModel.extend({ orderedBy: list.orderedBy, limit: list.limit, openGroupByDefault: list.openGroupByDefault, + parentID: list.id, type: 'list', viewType: list.viewType, }); @@ -2787,9 +2907,12 @@ var BasicModel = AbstractModel.extend({ if (list.orderedBy.length) { // sort records according to ordered_by[0] var order = list.orderedBy[0]; - list.data.sort(function (r1, r2) { - var data1 = self.localData[r1].data; - var data2 = self.localData[r2].data; + var data = list._changes || list.data; + data.sort(function (id1, id2) { + var r1 = self.localData[id1]; + var r2 = self.localData[id2]; + var data1 = r1._changes || r1.data; + var data2 = r2._changes || r2.data; if (data1[order.name] < data2[order.name]) { return order.asc ? -1 : 1; } diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 907ea7ddc07..8bbf9ae45a4 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -91,7 +91,7 @@ var BasicRenderer = AbstractRenderer.extend({ var record = state.id === id ? state : _.findWhere(state.data, {id: id}); if (!record) { - return this._render(); + return this._render().then(_.constant([])); } var defs = []; @@ -269,22 +269,6 @@ var BasicRenderer = AbstractRenderer.extend({ return widget.isValid() && (widget.isSet() || !modifiers.required); }, /** - * Updates the modifiers evaluation associated to a given modifiers data and - * a given record. This only updates the modifiers values. To see associated - * DOM updates: @see _updateAllModifiers @see _applyModifiers. - * - * @private - * @param {Object} modifiersData - * @param {Object} record - */ - _computeModifiers: function (modifiersData, record) { - var evalContext = record.evalContext; - modifiersData.evaluatedModifiers[record.id] - = _.mapObject(modifiersData.modifiers, function (modifier) { - return new Domain(modifier, evalContext).compute(evalContext); - }); - }, - /** * Destroys a given widget associated to the given record and removes it * from internal referencing. * @@ -342,7 +326,6 @@ var BasicRenderer = AbstractRenderer.extend({ * record. This allows nodes that will produce an AbstractField instance * to have their modifiers registered before this field creation as we * need the readonly modifier to be able to instantiate the AbstractField. - * (@see _computeModifiers). * * - On additional registrations, if the node was already registered but the * record is different, we evaluate the modifiers for this record and @@ -399,7 +382,7 @@ var BasicRenderer = AbstractRenderer.extend({ // Evaluate if necessary if (!modifiersData.evaluatedModifiers[record.id]) { - this._computeModifiers(modifiersData, record); + modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers); } // Element might not be given yet (a second call to the function can @@ -569,7 +552,6 @@ var BasicRenderer = AbstractRenderer.extend({ * 2) Updates the rendering of the view elements associated to the given * record to match the new modifiers. * - * @see _computeModifiers * @see _applyModifiers * * @private @@ -582,7 +564,7 @@ var BasicRenderer = AbstractRenderer.extend({ var defs = []; this.defs = defs; // Potentially filled by widget rerendering _.each(this.allModifiersData, function (modifiersData) { - self._computeModifiers(modifiersData, record); + modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers); self._applyModifiers(modifiersData, record); }); delete this.defs; diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index e38b7b4584e..736416dea27 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -67,21 +67,36 @@ var BasicView = AbstractView.extend({ var viewType = this.viewType; var viewFields = Object.keys(record.fieldsInfo[viewType]); var fieldNames = _.difference(viewFields, Object.keys(record.data)); + var fieldsInfo = record.fieldsInfo[viewType]; + // Suppose that in a form view, there is an x2many list view with - // an x2many field F of the related record, and that F is also - // displayed in the x2many form view (e.g. as a list or a kanban). - // In this case, F is represented in record.data - // (as it is known by the x2many list view), but its related fields - // (those displayed in the list or kanban) still need to be fetched. + // a field F, and that F is also displayed in the x2many form view. + // In this case, F is represented in record.data (as it is known by + // the x2many list view), but the loaded information may not suffice + // in the form view (e.g. if field is a many2many list in the form + // view, or if it is displayed by a widget requiring specialData). // So when this happens, F is added to the list of fieldNames to fetch. _.each(viewFields, function (name) { if (!_.contains(fieldNames, name)) { var fieldType = record.fields[name].type; + var fieldInfo = fieldsInfo[name]; + + // SpecialData case: field requires specialData that haven't + // been fetched yet. + if (fieldInfo.Widget) { + var requiresSpecialData = fieldInfo.Widget.prototype.specialData; + if (requiresSpecialData && !(name in record.specialData)) { + fieldNames.push(name); + return; + } + } + + // X2Many case: field is an x2many displayed as a list or + // kanban view, but the related fields haven't been loaded yet. if ((fieldType === 'one2many' || fieldType === 'many2many')) { if (!('fieldsInfo' in record.data[name])) { fieldNames.push(name); } else { - var fieldInfo = record.fieldsInfo[viewType][name]; var fieldViews = fieldInfo.views || fieldInfo.fieldsInfo || {}; var fieldViewTypes = Object.keys(fieldViews); var recordViewTypes = Object.keys(record.data[name].fieldsInfo); diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 64eae1f746a..939e44446a8 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -16,7 +16,7 @@ var scales = [ ]; function dateToServer (date) { - return date.clone().utc().format('YYYY-MM-DD HH:mm:ss'); + return date.clone().utc().locale('en').format('YYYY-MM-DD HH:mm:ss'); } return AbstractModel.extend({ diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js index ceedbb2df6e..9578e568558 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -43,7 +43,7 @@ var FieldManagerMixin = { */ _applyChanges: function (dataPointID, changes, event) { var self = this; - return this.model.notifyChanges(dataPointID, changes) + return this.model.notifyChanges(dataPointID, changes, event.data.viewType) .then(function (result) { if (event.data.force_save) { return self.model.save(dataPointID).then(function () { diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 4239edbb199..ce449f2dcc9 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -15,7 +15,6 @@ var FormController = BasicController.extend({ open_one2many_record: '_onOpenOne2ManyRecord', bounce_edit: '_onBounceEdit', button_clicked: '_onButtonClicked', - discard_x2m_changes: '_onDiscardX2MChanges', open_record: '_onOpenRecord', toggle_column_order: '_onToggleColumnOrder', }), @@ -51,9 +50,11 @@ var FormController = BasicController.extend({ * This method switches the form view in edit mode, with a new record. * * @todo make record creation a basic controller feature + * @param {string} [parentID] if given, the parentID will be used as parent + * for the new record. * @returns {Deferred} */ - createRecord: function () { + createRecord: function (parentID) { var self = this; var record = this.model.get(this.handle, {raw: true}); return this.model.load({ @@ -61,6 +62,7 @@ var FormController = BasicController.extend({ fields: record.fields, fieldsInfo: record.fieldsInfo, modelName: this.modelName, + parentID: parentID, res_ids: record.res_ids, type: 'record', viewType: 'form', @@ -231,6 +233,26 @@ var FormController = BasicController.extend({ } }, /** + * Override to disable buttons in the renderer. + * + * @override + * @private + */ + _disableButtons: function () { + this._super.apply(this, arguments); + this.renderer.disableButtons(); + }, + /** + * Override to enable buttons in the renderer. + * + * @override + * @private + */ + _enableButtons: function () { + this._super.apply(this, arguments); + this.renderer.enableButtons(); + }, + /** * Hook method, called when record(s) has been deleted. * * @override @@ -325,6 +347,8 @@ var FormController = BasicController.extend({ var self = this; var def; + this._disableButtons(); + var attrs = event.data.attrs; if (attrs.confirm) { var d = $.Deferred(); @@ -349,15 +373,14 @@ var FormController = BasicController.extend({ return self._callButtonAction(attrs, record); }); } - def.then(function () { - self.reload(); - }); if (event.data.show_wow) { def.then(function () { self.show_wow(); }); } + + def.always(this._enableButtons.bind(this)); }, /** * Called when the user wants to create a new record -> @see createRecord @@ -377,32 +400,6 @@ var FormController = BasicController.extend({ this.discardChanges(); }, /** - * Called when a x2m asks to discard the changes made to one of its row. - * - * @todo find a better way to handle this... this could also be used outside - * of form views - * - * @private - * @param {OdooEvent} ev - */ - _onDiscardX2MChanges: function (ev) { - var self = this; - ev.stopPropagation(); - var recordID = ev.data.recordID; - this.discardChanges(recordID) - .done(function () { - if (self.model.isNew(recordID)) { - self._abandonRecord(recordID); - } - // TODO this will tell the renderer to rerender the widget that - // asked for the discard but will unfortunately lose the click - // made on another row if any - self._confirmChange(self.handle, [ev.target.name], ev) - .always(ev.data.onSuccess); - }) - .fail(ev.data.onFailure); - }, - /** * Called when the user clicks on 'Duplicate Record' in the sidebar * * @private diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index a2c0d2129ab..ddd77ee5dc6 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -84,10 +84,27 @@ var FormRenderer = BasicRenderer.extend({ _.each(resetWidgets, function (widget) { self._setIDForLabel(widget, self.idsForLabels[widget.name]); }); + if (self.$('.o_field_invalid').length) { + self.canBeSaved(self.state.id); + } return resetWidgets; }); }, /** + * Disable stat buttons so that they can't be clicked anymore + * + */ + disableButtons: function() { + this.$('.oe_button_box button').attr('disabled', true); + }, + /** + * Enable stat buttons so they can be clicked again + * + */ + enableButtons: function() { + this.$('.oe_button_box button').removeAttr('disabled'); + }, + /** * returns the active tab pages for each notebook * * @todo currently, this method is unused... @@ -679,10 +696,12 @@ var FormRenderer = BasicRenderer.extend({ }, }); }); - return $('<div class="o_notebook">') + var $notebook = $('<div class="o_notebook">') .data('name', node.attrs.name || '_default_') - .append($headers) - .append($pages); + .append($headers, $pages); + this._registerModifiers(node, this.state, $notebook); + this._handleAttributes($notebook, node); + return $notebook; }, /** * @private diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 54e078ab368..b5c60fe9ab3 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -131,7 +131,8 @@ var KanbanController = BasicController.extend({ _updateButtons: function () { if (this.$buttons) { var data = this.model.get(this.handle, {raw: true}); - var createMuted = data.count === 0 && this.createColumnEnabled; + var grouped = data.groupedBy.length; + var createMuted = grouped && data.data.length === 0 && this.createColumnEnabled; this.$buttons.find('.o-kanban-button-new') .toggleClass('btn-primary', !createMuted) .toggleClass('btn-default', createMuted); @@ -260,7 +261,7 @@ var KanbanController = BasicController.extend({ this.trigger_up('execute_action', { action_data: attrs, model: record.model, - record_id: record.res_id, + res_ids: [record.res_id], on_closed: function () { self.model.reload(record.id).then(function (db_id) { var data = self.model.get(db_id); @@ -328,6 +329,7 @@ var KanbanController = BasicController.extend({ return self.model .addRecordToGroup(columnState.id, records[0]) .then(function (db_id) { + self._updateEnv(); column.addRecord(self.model.get(db_id), {position: 'before'}); }); } diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index f3d4d5b3d91..c606776ccc0 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -31,12 +31,20 @@ var KanbanModel = BasicModel.extend({ viewType: group.viewType, }); group.data.unshift(new_record.id); + group.res_ids.unshift(resId); group.count++; - return this._fetchRecord(new_record) - // .then(this._fetch_relational_data.bind(this)) - .then(function (result) { - return result.id; - }); + + // update the res_ids and count of the parent + var self = this; + var parent = this.localData[group.parentID]; + parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) { + return self.localData[dataPointID].res_ids; + })); + parent.count++; + + return this._fetchRecord(new_record).then(function (result) { + return result.id; + }); }, /** * Creates a new group from a name (performs a name_create). @@ -78,7 +86,6 @@ var KanbanModel = BasicModel.extend({ // newGroup.is_open = true; parent.data.push(newGroup.id); - parent.count++; return newGroup.id; }); }, 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 21e29780d09..7c83022cd60 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -64,7 +64,10 @@ var ListController = BasicController.extend({ return $.when(); } } - return this._super(recordID); + var self = this; + return this._super(recordID).then(function () { + self._updateButtons('readonly'); + }); }, /** * Calculate the active domain of the list view. This should be done only @@ -89,7 +92,8 @@ var ListController = BasicController.extend({ contexts: [userContext].concat(searchData.contexts), group_by_seq: searchData.groupbys || [] }); - return $.when(self.dataset.domain.concat(results.domain || [])); + var record = self.model.get(self.handle, {raw: true}); + return $.when(record.getDomain().concat(results.domain || [])); } else { return $.Deferred().resolve(); } @@ -190,19 +194,25 @@ var ListController = BasicController.extend({ } }, /** - * Add a record to the list + * Adds a record to the list. + * Disables the buttons to prevent concurrent record creation or edition. * * @todo make record creation a basic controller feature * @private */ _addRecord: function () { var self = this; - this.model.addDefaultRecord(this.handle, {position: this.editable}).then(function (recordID) { + this._disableButtons(); + return this.renderer.unselectRow().then(function () { + return self.model.addDefaultRecord(self.handle, { + position: self.editable, + }); + }).then(function (recordID) { self._toggleNoContentHelper(false); var state = self.model.get(self.handle); self.renderer.updateState(state, {}); self.renderer.editRecord(recordID); - }); + }).always(this._enableButtons.bind(this)); }, /** * Archive the current selection 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 a857fee7103..a037c12a96e 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 @@ -84,6 +84,26 @@ ListRenderer.include({ return this._super(recordID); }, /** + * We need to override the confirmChange method from BasicRenderer to + * reevaluate the row decorations. Since they depends on the current value + * of the row, they might have changed between each edit. + * + * @override + */ + confirmChange: function (state, id) { + var self = this; + return this._super.apply(this, arguments).then(function (widgets) { + if (widgets.length) { + var rowIndex = _.findIndex(state.data, function (r) { + return r.id === id; + }); + var $row = self.$('.o_data_row:nth(' + rowIndex + ')'); + self._setDecorationClasses(state.data[rowIndex], $row); + } + return widgets; + }); + }, + /** * Edit a given record in the list * * @param {string} recordID @@ -113,13 +133,15 @@ ListRenderer.include({ */ removeLine: function (state, recordID) { var rowIndex = _.findIndex(this.state.data, {id: recordID}); + this.state = state; + if (rowIndex === -1) { + return; + } if (rowIndex === this.currentRow) { this.currentRow = null; } var $row = this.$('.o_data_row:nth(' + rowIndex + ')'); $row.remove(); - - this.state = state; }, /** * Updates the already rendered row associated to the given recordID so that @@ -202,6 +224,44 @@ ListRenderer.include({ return $.when.apply($, defs); }, + /** + * This method is called whenever we click/move outside of a row that was + * in edit mode. This is the moment we save all accumulated changes on that + * row, if needed (@see BasicController.saveRecord). + * + * Note that we have to disable the focusable elements (inputs, ...) to + * prevent subsequent editions. These edits would be lost, because the list + * view only saves records when unselecting a row. + * + * @returns {Deferred} The deferred resolves if the row was unselected (and + * possibly removed). If may be rejected, when the row is dirty and the + * user refuses to discard its changes. + */ + unselectRow: function () { + // Protect against calling this method when no row is selected + if (this.currentRow === null) { + return $.when(); + } + + var record = this.state.data[this.currentRow]; + var recordWidgets = this.allFieldWidgets[record.id]; + toggleWidgets(true); + + var def = $.Deferred(); + this.trigger_up('save_line', { + recordID: record.id, + onSuccess: def.resolve.bind(def), + onFailure: def.reject.bind(def), + }); + return def.fail(toggleWidgets.bind(null, false)); + + function toggleWidgets(disabled) { + _.each(recordWidgets, function (widget) { + var $el = widget.getFocusableElement(); + $el.prop('disabled', disabled); + }); + } + }, //-------------------------------------------------------------------------- // Private @@ -241,7 +301,7 @@ ListRenderer.include({ if (this.currentRow > 0) { this._selectCell(this.currentRow - 1, this.columns.length - 1); } else { - this._unselectRow().then(this.trigger_up.bind(this, 'add_record')); + this.unselectRow().then(this.trigger_up.bind(this, 'add_record')); } }, /** @@ -251,10 +311,16 @@ ListRenderer.include({ * @private */ _moveToNextLine: function () { + var record = this.state.data[this.currentRow]; + var fieldNames = this.canBeSaved(record.id); + if (fieldNames.length) { + return; + } + if (this.currentRow < this.state.data.length - 1) { this._selectCell(this.currentRow + 1, 0); } else { - this._unselectRow().then(this.trigger_up.bind(this, 'add_record')); + this.unselectRow().then(this.trigger_up.bind(this, 'add_record')); } }, /** @@ -399,7 +465,7 @@ ListRenderer.include({ // 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 () { // Notify the controller we want to make a record editable var record = self.state.data[rowIndex]; var def = $.Deferred(); @@ -410,44 +476,6 @@ ListRenderer.include({ return def; }); }, - /** - * This method is called whenever we click/move outside of a row that was - * in edit mode. This is the moment we save all accumulated changes on that - * row, if needed (@see BasicController.saveRecord). - * - * Note that we have to disable the focusable elements (inputs, ...) to - * prevent subsequent editions. These edits would be lost, because the list - * view only saves records when unselecting a row. - * - * @returns {Deferred} The deferred resolves if the row was unselected (and - * possibly removed). If may be rejected, when the row is dirty and the - * user refuses to discard its changes. - */ - _unselectRow: function () { - // Protect against calling this method when no row is selected - if (this.currentRow === null) { - return $.when(); - } - - var record = this.state.data[this.currentRow]; - var recordWidgets = this.allFieldWidgets[record.id]; - toggleWidgets(true); - - var def = $.Deferred(); - this.trigger_up('save_line', { - recordID: record.id, - onSuccess: def.resolve.bind(def), - onFailure: def.reject.bind(def), - }); - return def.fail(toggleWidgets.bind(null, false)); - - function toggleWidgets(disabled) { - _.each(recordWidgets, function (widget) { - var $el = widget.getFocusableElement(); - $el.prop('disabled', disabled); - }); - } - }, //-------------------------------------------------------------------------- // Handlers @@ -469,7 +497,7 @@ ListRenderer.include({ // but we do want to unselect current row var self = this; - this._unselectRow().then(function () { + this.unselectRow().then(function () { self.trigger_up('add_record'); // TODO write a test, the deferred was not considered }); }, @@ -495,7 +523,7 @@ ListRenderer.include({ * We need to manually unselect row, because noone else would do it */ _onEmptyRowClick: function () { - this._unselectRow(); + this.unselectRow(); }, /** * Clicking on a footer should unselect (and save) the currently selected @@ -503,7 +531,7 @@ ListRenderer.include({ * and _onWindowClicked ignore those clicks. */ _onFooterClick: function () { - this._unselectRow(); + this.unselectRow(); }, /** * Handles the keyboard navigation according to events triggered by field @@ -568,6 +596,11 @@ ListRenderer.include({ case 'next_line': this._moveToNextLine(); break; + case 'cancel': + this.trigger_up('discard_changes', { + recordID: ev.target.dataPointID, + }); + break; } }, /** @@ -644,7 +677,7 @@ ListRenderer.include({ return; } - this._unselectRow(); + this.unselectRow(); }, }); 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 21028b99e02..e589c0d430a 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -6,7 +6,6 @@ var config = require('web.config'); var core = require('web.core'); var field_utils = require('web.field_utils'); var Pager = require('web.Pager'); -var session = require('web.session'); var utils = require('web.utils'); var _t = core._t; @@ -147,32 +146,6 @@ var ListRenderer = BasicRenderer.extend({ } }, /** - * Each line can be decorated according to a few simple rules. The arch - * description of the list may have one of the decoration-X attribute with - * a domain as value. Then, for each record, we check if the domain matches - * the record, and add the text-X css class to the element. This method is - * concerned with the computation of the list of css classes for a given - * record. - * - * @private - * @param {Object} record a basic model record - * @returns {string[]} a list of css classes - */ - _computeDecorationClassNames: function (record) { - var data = JSON.parse(JSON.stringify(record.data)); - var context = _.extend({}, data, { - uid: session.uid, - current_date: moment().format('YYYY-MM-DD') - // TODO: time, datetime, relativedelta - }); - return _.chain(this.rowDecorations) - .pick(function (expr) { - return py.PY_isTrue(py.evaluate(expr, context)); - }).map(function (expr, decoration) { - return decoration.replace('decoration', 'text'); - }).value(); - }, - /** * return the number of visible columns. Note that this number depends on * the state of the renderer. For example, in editable mode, it could be * one more that in non editable mode, because there may be a visible 'trash @@ -201,7 +174,9 @@ var ListRenderer = BasicRenderer.extend({ var field = self.state.fields[column.attrs.name]; var value = aggregateValues[column.attrs.name].value; var help = aggregateValues[column.attrs.name].help; - var formattedValue = field_utils.format[field.type](value, field, {}); + var formattedValue = field_utils.format[field.type](value, field, { + escape: true, + }); $cell.addClass('o_list_number').attr('title', help).html(formattedValue); } return $cell; @@ -276,7 +251,11 @@ var ListRenderer = BasicRenderer.extend({ var name = node.attrs.name; var field = this.state.fields[name]; var value = record.data[name]; - var formattedValue = field_utils.format[field.type](value, field, { data: record.data }); + var formattedValue = field_utils.format[field.type](value, field, { + data: record.data, + escape: true, + isPassword: 'password' in node.attrs, + }); return $td.html(formattedValue); }, /** @@ -539,17 +518,14 @@ var ListRenderer = BasicRenderer.extend({ return self._renderBodyCell(record, node, index, {mode: 'readonly'}); }); delete this.defs; - var className = 'o_data_row'; - var decorations = this._computeDecorationClassNames(record); - if (decorations.length) { - className += (' ' + decorations.join(' ')); - } - var $tr = $('<tr/>', {class: className}) + + var $tr = $('<tr/>', {class: 'o_data_row'}) .data('id', record.id) .append($cells); if (this.hasSelectors) { $tr.prepend(this._renderSelector('td')); } + this._setDecorationClasses(record, $tr); return $tr; }, /** @@ -621,6 +597,24 @@ var ListRenderer = BasicRenderer.extend({ return this._super(); }, /** + * Each line can be decorated according to a few simple rules. The arch + * description of the list may have one of the decoration-X attribute with + * a domain as value. Then, for each record, we check if the domain matches + * the record, and add the text-X css class to the element. This method is + * concerned with the computation of the list of css classes for a given + * record. + * + * @private + * @param {Object} record a basic model record + * @param {jQueryElement} $tr a jquery <tr> element (the row to add decoration) + */ + _setDecorationClasses: function (record, $tr) { + _.each(this.rowDecorations, function (expr, decoration) { + var cssClass = decoration.replace('decoration', 'text'); + $tr.toggleClass(cssClass, py.PY_isTrue(py.evaluate(expr, record.evalContext))); + }); + }, + /** * Whenever we change the state of the selected rows, we need to call this * method to keep the this.selection variable in sync, and also to recompute * the aggregates. diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index f132704a475..fb175aa1934 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -74,8 +74,9 @@ var PivotController = AbstractController.extend({ //-------------------------------------------------------------------------- /** - * We save the current measure and group bys, so we can restore the view - * when we save the current state in the search view. + * Returns the current measures and groupbys, so we can restore the view + * when we save the current state in the search view, or when we add it to + * the dashboard. * * @override method from AbstractController * @returns {Object} @@ -84,8 +85,8 @@ var PivotController = AbstractController.extend({ var state = this.model.get(); return { pivot_measures: state.measures, - pivot_column_groupby: state.data.main_col.groupbys, - pivot_row_groupby: state.data.main_row.groupbys, + pivot_column_groupby: state.colGroupBys, + pivot_row_groupby: state.rowGroupBys, }; }, /** @@ -126,6 +127,7 @@ var PivotController = AbstractController.extend({ return; } framework.blockUI(); + table.title = this.title; session.get_file({ url: '/web/pivot/export_xls', data: {data: JSON.stringify(table)}, @@ -218,8 +220,9 @@ var PivotController = AbstractController.extend({ this.update({}, {reload: false}); } if ($target.hasClass('o_pivot_expand_button')) { - this.model.expandAll(); - this.update({reload: false}); + this.model + .expandAll() + .then(this.update.bind(this, {}, {reload: false})); } if ($target.parents('.o_pivot_measures_list').length) { var parent = $target.parent(); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index ab4a2eb0314..6bbadd56ed8 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -127,20 +127,21 @@ var PivotModel = AbstractModel.extend({ }); }, /** + * Export the current pivot view in a simple JS object. + * * @returns {Object} */ exportData: function () { - var record = this.data; - var measureNbr = record.measures.length; - var headers = this.renderer._computeHeaders(); + var measureNbr = this.data.measures.length; + var headers = this._computeHeaders(); var measureRow = measureNbr > 1 ? _.last(headers) : []; - var rows = this.renderer._computeRows(); + var rows = this._computeRows(); var i, j, value; headers[0].splice(0,1); // process measureRow for (i = 0; i < measureRow.length; i++) { - measureRow[i].measure = this.measures[measureRow[i].measure].string; + measureRow[i].measure = this.fields[measureRow[i].measure].string; } // process all rows for (i =0, j, value; i < rows.length; i++) { @@ -148,7 +149,7 @@ var PivotModel = AbstractModel.extend({ value = rows[i].values[j]; rows[i].values[j] = { is_bold: (i === 0) || - ((record.data.main_col.width > 1) && + ((this.data.main_col.width > 1) && (j >= rows[i].values.length - measureNbr)), value: (value === undefined) ? "" : value, }; @@ -159,7 +160,6 @@ var PivotModel = AbstractModel.extend({ measure_row: measureRow, rows: rows, nbr_measures: measureNbr, - title: this.title, }; }, /** @@ -221,17 +221,14 @@ var PivotModel = AbstractModel.extend({ load: function (params) { this.initialDomain = params.domain; this.initialRowGroupBys = params.rowGroupBys; - this.initialColGroupBys = params.colGroupBys; - this.initialMeasures = params.measures; this.fields = params.fields; this.modelName = params.modelName; - var groupedBy = params.groupedBy.length ? params.groupedBy : this.initialRowGroupBys; this.data = { domain: params.domain, context: params.context, - groupedBy: groupedBy, - colGroupBys: params.colGroupBys || this.initialColGroupBys, - measures: this.initialMeasures, + groupedBy: params.rowGroupBys, + colGroupBys: params.colGroupBys, + measures: params.measures, sorted_column: {}, }; return this._loadData(); @@ -514,7 +511,7 @@ var PivotModel = AbstractModel.extend({ _loadData: function () { var self = this; var groupBys = []; - var rowGroupBys = this.data.groupedBy; + var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys; var colGroupBys = this.data.colGroupBys; var fields = [].concat(rowGroupBys, colGroupBys, this.data.measures); @@ -591,7 +588,7 @@ var PivotModel = AbstractModel.extend({ }); var index = 0; - var rowGroupBys = this.data.groupedBy; + var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys; var colGroupBys = this.data.colGroupBys; var datapt, row, col, attrs, cell_value; var main_row_header, main_col_header; diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index 83bd32de830..d90c0d21780 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -87,9 +87,9 @@ var PivotView = AbstractView.extend({ activeMeasures.push('__count'); } - this.loadParams.measures = activeMeasures; - this.loadParams.colGroupBys = colGroupBys; - this.loadParams.rowGroupBys = rowGroupBys; + this.loadParams.measures = params.context.pivot_measures || activeMeasures; + this.loadParams.colGroupBys = params.context.pivot_column_groupby || colGroupBys; + this.loadParams.rowGroupBys = params.context.pivot_row_groupby || rowGroupBys; this.loadParams.fields = fields; this.controllerParams.title = params.title || arch.attrs.string || _t("Untitled"); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 7e20cd74d04..b84d60d5b7c 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -133,7 +133,7 @@ var FormViewDialog = ViewDialog.extend({ text: _t("Save & New"), classes: "btn-primary", click: function () { - this._save().then(self.form_view.createRecord.bind(self.form_view)); + this._save().then(self.form_view.createRecord.bind(self.form_view, self.parentID)); }, }); } @@ -375,11 +375,9 @@ var SelectCreateDialog = ViewDialog.extend({ }); }, _process_search_data: function (domains, contexts, groupbys) { - var user_context = this.getSession().user_context; - contexts = [user_context].concat(contexts); var results = pyeval.eval_domains_and_contexts({ - domains: domains || [], - contexts: contexts || [], + domains: [this.domain].concat(domains), + contexts: [this.context].concat(contexts), group_by_seq: groupbys || [] }); return { diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index 13ab8fd0892..c11551ca2cc 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -295,7 +295,10 @@ var DataExport = Dialog.extend({ } var self = this; - return this.exports.read_slice(['name'], { + return this._rpc({ + model: 'ir.exports', + method: 'search_read', + fields: ['name'], domain: [['resource', '=', this.record.model]] }).then(function (export_list) { if (!export_list.length) { diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 3b191a0837a..34ce34e6ad8 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -435,6 +435,7 @@ .o_field_widget { text-align: right; + .o-justify-content(flex-end); width: 100%; } } diff --git a/addons/web/static/src/less/layout_background.less b/addons/web/static/src/less/layout_background.less index 7ff90a68921..6750705c35b 100644 --- a/addons/web/static/src/less/layout_background.less +++ b/addons/web/static/src/less/layout_background.less @@ -34,7 +34,7 @@ background-size: cover; background-position: bottom center; background-repeat: no-repeat; - min-height: 920px; + min-height: 900px; thead tr th, h2 { color: @odoo-brand-primary; } diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index eaf0f1bfba8..a09b8e7ac0c 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -91,17 +91,16 @@ } // Values styling - .o_data_cell { - &.o_list_number { - text-align: right; - white-space: nowrap; - } - &.o_list_text { - white-space: pre-wrap; - } - &.o_invisible_modifier { - display: initial!important; // Override invisible to keep the cells aligned - } + .o_list_number { + text-align: right; + white-space: nowrap; + } + .o_list_text { + white-space: pre-wrap; + } + + .o_data_cell.o_invisible_modifier { + display: initial!important; // Override invisible to keep the cells aligned } //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 50e59713562..394abe18c7c 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -604,7 +604,7 @@ <div t-if="!widget.readonly" class="o_domain_leaf_edition"> <!-- field selector will be instantiated here --> <div> <!-- used for flex stretching --> - <select class="o_domain_leaf_operator_select"> + <select class="o_domain_leaf_operator_select o_input"> <option t-foreach="widget.operators" t-as="key" t-att-value="key" t-att-selected="widget.displayOperator === key ? 'selected' : None"> @@ -614,7 +614,7 @@ </div> <div t-attf-class="o_ds_value_cell#{_.contains(['set', 'not set'], widget.displayOperator) ? ' hidden' : ''}"> <t t-if="widget.selectionChoices !== null"> - <select class="o_domain_leaf_value_input"> + <select class="o_domain_leaf_value_input o_input"> <option t-foreach="widget.selectionChoices" t-as="val" t-att-value="val[0]" t-att-selected="_.contains(val, widget.displayValue) ? 'selected' : None"> @@ -630,12 +630,12 @@ </span> </div> <div class="o_domain_leaf_value_tags"> - <input placeholder="Add new value" type="text"/> + <input placeholder="Add new value" type="text" class="o_input"/> <button class="btn btn-xs btn-primary fa fa-plus o_domain_leaf_value_add_tag_button"/> </div> </t> <t t-else=""> - <input class="o_domain_leaf_value_input" type="text" t-att-value="widget.displayValue"/> + <input class="o_domain_leaf_value_input o_input" type="text" t-att-value="widget.displayValue"/> </t> </t> </div> @@ -730,7 +730,8 @@ <div t-if="widget.mode !== 'readonly'" t-attf-class="o_field_radio #{widget.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'}"> <div t-foreach="widget.values" t-as="value" class="o_radio_item"> - <input type="radio" class="o_radio_input" t-att-id="widget.unique_id + '_' + value[0]" t-att-name="widget.unique_id" t-att-data-value="value[0]"/> + <input type="radio" class="o_radio_input" t-att-id="widget.unique_id + '_' + value[0]" + t-att-name="widget.unique_id" t-att-data-value="value[0]" t-att-data-index="value_index"/> <label t-if="!widget.nodeOptions.no_radiolabel" class="o_form_label" t-att-for="widget.unique_id + '_' + value[0]"><t t-esc="value[1]"/></label> </div> </div> @@ -1180,11 +1181,12 @@ <t t-name="FieldMany2ManyCheckBoxes"> <div> <div t-foreach="widget.m2mValues" t-as="m2m_value"> + <t t-set="id_for_label" t-value="'o_many2many_checkbox_' + _.uniqueId()"/> <div class="o_checkbox"> - <input type="checkbox" t-att-data-record-id="JSON.stringify(m2m_value[0])"/> + <input type="checkbox" t-att-id="id_for_label" t-att-data-record-id="JSON.stringify(m2m_value[0])"/> <span/> </div> - <label class="o_form_label"><t t-esc="m2m_value[1]"/></label> + <label t-att-for="id_for_label" class="o_form_label"><t t-esc="m2m_value[1]"/></label> </div> </div> </t> diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7b9fbd061eb..07d13fbe93a 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1,6 +1,7 @@ odoo.define('web.basic_fields_tests', function (require) { "use strict"; +var basicFields = require('web.basic_fields'); var concurrency = require('web.concurrency'); var core = require('web.core'); var FormView = require('web.FormView'); @@ -10,6 +11,7 @@ var session = require('web.session'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; +var DebouncedField = basicFields.DebouncedField; var _t = core._t; QUnit.module('fields', {}, function () { @@ -120,6 +122,66 @@ QUnit.module('basic_fields', { } }, function () { + QUnit.module('DebouncedField'); + + QUnit.test('debounced fields do not trigger call _setValue once destroyed', function (assert) { + var done = assert.async(); + assert.expect(4); + + var def = $.Deferred(); + var _doAction = DebouncedField.prototype._doAction; + DebouncedField.prototype._doAction = function () { + _doAction.apply(this, arguments); + def.resolve(); + }; + var _setValue = DebouncedField.prototype._setValue; + DebouncedField.prototype._setValue = function () { + assert.step('_setValue'); + _setValue.apply(this, arguments); + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + fieldDebounce: 3, + viewOptions: { + mode: 'edit', + }, + }); + + // change the value + form.$('input').val('new value').trigger('input'); + assert.verifySteps([], "_setValue shouldn't have been called yet"); + + // save + form.$buttons.find('.o_form_button_save').click(); + assert.verifySteps(['_setValue'], "_setValue should have been called once"); + + // destroy the form view + def = $.Deferred(); + form.destroy(); + + // wait for the debounced callback to be called + def.then(function () { + assert.verifySteps(['_setValue'], + "_setValue should not have been called after widget destruction"); + + DebouncedField.prototype._doAction = _doAction; + DebouncedField.prototype._setValue = _setValue; + done(); + }); + + }); + QUnit.module('FieldBoolean'); QUnit.test('boolean field in form view', function (assert) { @@ -710,6 +772,33 @@ QUnit.module('basic_fields', { _t.database.multi_lang = multiLang; }); + QUnit.test('char field does not allow html injections', function (assert) { + assert.expect(1); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('input').val('<script>throw Error();</script>').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_field_widget').text(), '<script>throw Error();</script>', + 'the value should have been properly escaped'); + + form.destroy(); + }); QUnit.module('UrlWidget'); @@ -1742,6 +1831,37 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('monetary field with monetary field given in options', function (assert) { + assert.expect(1); + + this.data.partner.fields.qux.type = "monetary"; + this.data.partner.fields.company_currency_id = { + string: "Company Currency", type: "many2one", relation: "currency", + }; + this.data.partner.records[4].company_currency_id = 2; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" options="{\'currency_field\': \'company_currency_id\'}"/>' + + '<field name="company_currency_id"/>' + + '</sheet>' + + '</form>', + res_id: 5, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + assert.strictEqual(form.$('.o_field_monetary').html(), "9.10 €", + "field monetary should be formatted with correct currency"); + + form.destroy(); + }); + QUnit.test('should keep the focus when being edited in x2many lists', function (assert) { assert.expect(6); @@ -2166,6 +2286,42 @@ QUnit.module('basic_fields', { list.destroy(); }); + QUnit.test('phone field does not allow html injections', function (assert) { + assert.expect(1); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="phone"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + config: { + device: { + size_class: 0, + SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, + } + }, + }); + + var val = '<script>throw Error();</script><script>throw Error();</script>'; + form.$('input').val(val).trigger('input'); + + // save + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_field_widget').text().split('\u00AD').join(''), val, + "value should have been correctly escaped"); + + form.destroy(); + }); QUnit.module('PriorityWidget'); diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js index bfd60000035..e00d45ef91f 100644 --- a/addons/web/static/tests/fields/field_utils_tests.js +++ b/addons/web/static/tests/fields/field_utils_tests.js @@ -101,7 +101,7 @@ QUnit.test('format one2many', function(assert) { }); QUnit.test('parse float', function(assert) { - assert.expect(6); + assert.expect(7); assert.strictEqual(fieldUtils.parse.float(""), 0); assert.strictEqual(fieldUtils.parse.float("0"), 0); @@ -109,6 +109,16 @@ QUnit.test('parse float', function(assert) { assert.strictEqual(fieldUtils.parse.float("-100.00"), -100); assert.strictEqual(fieldUtils.parse.float("1,000.00"), 1000); assert.strictEqual(fieldUtils.parse.float("1,000,000.00"), 1000000); + + var originalParameters = $.extend(true, {}, core._t.database.parameters); + _.extend(core._t.database.parameters, { + grouping: [3, 0], + decimal_point: ',', + thousands_sep: '.' + }); + assert.strictEqual(fieldUtils.parse.float('1.234,567'), 1234.567); + + core._t.database.parameters = originalParameters; }); QUnit.test('parse monetary', function(assert) { diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 44b41219d52..73aa28730e2 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1,8 +1,10 @@ odoo.define('web.relational_fields_tests', function (require) { "use strict"; +var concurrency = require('web.concurrency'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); +var relationalFields = require('web.relational_fields'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; @@ -20,7 +22,7 @@ QUnit.module('relational_fields', { int_field: {string: "int_field", type: "integer", sortable: true}, qux: {string: "Qux", type: "float", digits: [16,1] }, p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'}, - turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle'}, + turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu'}, trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, product_id: {string: "Product", type: "many2one", relation: 'product'}, @@ -125,7 +127,7 @@ QUnit.module('relational_fields', { }, user: { fields: { - name: {string: "Name", type: "char"} + name: {string: "Name", type: "char"}, }, records: [{ id: 17, @@ -197,6 +199,56 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchanges on many2ones trigger when editing record in form view', function (assert) { + assert.expect(9); + + this.data.partner.onchanges.user_id = function () {}; + this.data.user.fields.other_field = {string: "Other Field", type: "char"}; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="user_id"/>' + + '</group>' + + '</sheet>' + + '</form>', + archs: { + 'user,false,form': '<form string="Users"><field name="other_field"/></form>', + }, + res_id: 1, + mockRPC: function (route, args) { + assert.step(args.method); + if (args.method === 'get_formview_id') { + return $.when(false); + } + if (args.method === 'onchange') { + assert.strictEqual(args.args[1].user_id, 17, + "onchange is triggered with correct user_id"); + } + return this._super(route, args); + }, + }); + + // open the many2one in form view and change something + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_external_button').click(); + $('.modal-body input[name="other_field"]').val('wood').trigger('input'); + + // save the modal and make sure an onchange is triggered + $('.modal .modal-footer .btn-primary').first().click(); + assert.verifySteps(['read', 'get_formview_id', 'read', 'write', 'onchange', 'read']); + + // save the main record, and check that no extra rpcs are done (record + // is not dirty, only a related record was modified) + form.$buttons.find('.o_form_button_save').click(); + assert.verifySteps(['read', 'get_formview_id', 'read', 'write', 'onchange', 'read']); + form.destroy(); + }); + + QUnit.test('many2one readonly fields with option "no_open"', function (assert) { assert.expect(1); @@ -579,6 +631,191 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('pressing enter in a m2o in an editable list', function (assert) { + assert.expect(9); + var done = assert.async(); + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="product_id"/></tree>', + }); + + list.$('td.o_data_cell:first').click(); + assert.strictEqual(list.$('.o_selected_row').length, 1, + "should have a row in edit mode"); + + // we now write 'a' and press enter to check that the selection is + // working, and prevent the navigation + list.$('td.o_data_cell input:first').val('a').trigger('input'); + concurrency.delay(0).then(function () { + var $input = list.$('td.o_data_cell input:first'); + var $dropdown = $input.autocomplete('widget'); + assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible"); + + // we now trigger ENTER to select first choice + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + assert.strictEqual($input[0], document.activeElement, + "input should still be focused"); + + // we now trigger again ENTER to make sure we can move to next line + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + + assert.notOk(document.contains($input[0]), + "input should no longer be in dom"); + assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'), + "second row should now be selected"); + + // we now write again 'a' in the cell to select xpad. We will now + // test with the tab key + list.$('td.o_data_cell input:first').val('a').trigger('input'); + return concurrency.delay(0); + }).then(function () { + var $input = list.$('td.o_data_cell input:first'); + var $dropdown = $input.autocomplete('widget'); + assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible"); + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.TAB, + keyCode: $.ui.keyCode.TAB, + })); + assert.strictEqual($input[0], document.activeElement, + "input should still be focused"); + + // we now trigger again ENTER to make sure we can move to next line + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.TAB, + keyCode: $.ui.keyCode.TAB, + })); + + assert.notOk(document.contains($input[0]), + "input should no longer be in dom"); + assert.ok(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'), + "third row should now be selected"); + list.destroy(); + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + done(); + }); + }); + + QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) { + assert.expect(6); + var done = assert.async(); + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + + this.data.partner.onchanges.product_id = function (obj) { + obj.int_field = obj.product_id || 0; + }; + + var def = $.Deferred(); + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="product_id"/><field name="int_field"/></tree>', + mockRPC: function (route, args) { + if (args.method) { + assert.step(args.method); + } + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return def.then(_.constant(result)); + } + return result; + }, + }); + + list.$('td.o_data_cell:first').click(); + list.$('td.o_data_cell input:first').val('a').trigger('input'); + concurrency.delay(0).then(function () { + var $input = list.$('td.o_data_cell input:first'); + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + $input.trigger($.Event('keyup', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + def.resolve(); + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + assert.strictEqual($('div.modal').length, 0, "should not have any modal in DOM"); + assert.verifySteps(['name_search', 'onchange', 'write', 'read']); + list.destroy(); + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + done(); + }); + }); + + QUnit.test('many2one in editable list + onchange, with enter, part 2 [REQUIRE FOCUS]', function (assert) { + // this is the same test as the previous one, but the onchange is just + // resolved slightly later + assert.expect(6); + var done = assert.async(); + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + + this.data.partner.onchanges.product_id = function (obj) { + obj.int_field = obj.product_id || 0; + }; + + var def = $.Deferred(); + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="product_id"/><field name="int_field"/></tree>', + mockRPC: function (route, args) { + if (args.method) { + assert.step(args.method); + } + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return def.then(_.constant(result)); + } + return result; + }, + }); + + list.$('td.o_data_cell:first').click(); + list.$('td.o_data_cell input:first').val('a').trigger('input'); + concurrency.delay(0).then(function () { + var $input = list.$('td.o_data_cell input:first'); + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + $input.trigger($.Event('keyup', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + def.resolve(); + assert.strictEqual($('div.modal').length, 0, "should not have any modal in DOM"); + assert.verifySteps(['name_search', 'onchange', 'write', 'read']); + list.destroy(); + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + done(); + }); + }); + QUnit.module('FieldOne2Many'); QUnit.test('one2many basic properties', function (assert) { @@ -1336,12 +1573,14 @@ QUnit.module('relational_fields', { QUnit.test('one2many list (editable): edition, part 4', function (assert) { assert.expect(3); + var i = 0; this.data.turtle.onchanges = { turtle_trululu: function (obj) { - if (obj.turtle_trululu) { + if (i) { obj.turtle_description = "Some Description"; } + i++; }, }; @@ -1481,6 +1720,44 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('pressing enter in a o2m with a required empty m2o', function (assert) { + assert.expect(4); + + this.data.turtle.fields.turtle_foo.required = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<group>' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</group>' + + '</form>', + res_id: 2, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + // edit mode, then click on Add an item, then click elsewhere + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input[name="turtle_foo"]').trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + assert.ok(form.$('input[name="turtle_foo"]').hasClass('o_field_invalid'), + "input should be marked invalid"); + assert.verifySteps(['read', 'default_get']); + form.destroy(); + }); + QUnit.test('editing a o2m, with required field and onchange', function (assert) { assert.expect(12); @@ -1537,6 +1814,74 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable o2m, pressing ESC discard current changes', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 2, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "there should be one data row"); + + form.$('input[name="turtle_foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); + assert.strictEqual(form.$('tr.o_data_row').length, 0, + "data row should have been discarded"); + assert.verifySteps(['read', 'default_get']); + form.destroy(); + }); + + QUnit.test('editable o2m with required field, pressing ESC discard current changes', function (assert) { + assert.expect(5); + + this.data.turtle.fields.turtle_foo.required = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 2, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "there should be one data row"); + + form.$('input[name="turtle_foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); + assert.strictEqual(form.$('tr.o_data_row').length, 0, + "data row should have been discarded"); + assert.verifySteps(['read', 'default_get']); + form.destroy(); + }); + QUnit.test('onchange in a one2many', function (assert) { assert.expect(1); @@ -2036,6 +2381,45 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many field with context', function (assert) { + assert.expect(2); + + var counter = 0; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<group>' + + '<field name="turtles" context="{\'turtles\':turtles}">' + + '<tree editable="bottom">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</group>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'default_get') { + var expected = counter === 0 ? + [[4, 2, false]] : + [[4, 2, false], [0, false, {display_name: false, turtle_foo: 'hammer'}]]; + assert.deepEqual(args.kwargs.context.turtles, expected, + "should have properly evaluated turtles key in context"); + counter++; + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input[name="turtle_foo"]').val('hammer').trigger('input'); + form.$('.o_field_x2many_list_row_add a').click(); + form.destroy(); + }); + QUnit.test('one2many list edition, some basic functionality', function (assert) { assert.expect(3); @@ -2334,25 +2718,26 @@ QUnit.module('relational_fields', { }); QUnit.test('parent data is properly sent on an onchange rpc, new record', function (assert) { - assert.expect(1); + assert.expect(6); - this.data.partner.onchanges = {bar: function () {}}; + this.data.turtle.onchanges = {turtle_bar: function () {}}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '<form string="Partners">' + '<field name="foo"/>' + - '<field name="p">' + + '<field name="turtles">' + '<tree editable="top">' + - '<field name="bar"/>' + + '<field name="turtle_bar"/>' + '</tree>' + '</field>' + '</form>', mockRPC: function (route, args) { - if (args.method === 'onchange') { + assert.step(args.method); + if (args.method === 'onchange' && args.model === 'turtle') { var fieldValues = args.args[1]; - assert.strictEqual(fieldValues.trululu.foo, "My little Foo Value", + assert.strictEqual(fieldValues.turtle_trululu.foo, "My little Foo Value", "should have properly sent the parent foo value"); } return this._super.apply(this, arguments); @@ -2361,6 +2746,7 @@ QUnit.module('relational_fields', { form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); + assert.verifySteps(['default_get', 'onchange', 'default_get', 'onchange']); form.destroy(); }); @@ -2417,7 +2803,7 @@ QUnit.module('relational_fields', { res_id: 1, intercepts: { execute_action: function (event) { - assert.strictEqual(event.data.record_id, 2, + assert.deepEqual(event.data.res_ids, [2], 'should call with correct id'); assert.strictEqual(event.data.model, 'partner', 'should call with correct model'); @@ -2461,7 +2847,7 @@ QUnit.module('relational_fields', { res_id: 1, intercepts: { execute_action: function (event) { - assert.strictEqual(event.data.record_id, 2, + assert.deepEqual(event.data.res_ids, [2], 'should call with correct id'); assert.strictEqual(event.data.model, 'partner', 'should call with correct model'); @@ -2478,7 +2864,7 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('one2many kanban with edit type action and domain widget (widget wich use SpecialData)', function (assert) { + QUnit.test('one2many kanban with edit type action and domain widget (widget using SpecialData)', function (assert) { assert.expect(1); this.data.turtle.fields.model_name = {string: "Domain Condition Model", type: "char"}; @@ -2500,6 +2886,8 @@ QUnit.module('relational_fields', { '<t t-name="kanban-box">' + '<div><field name="display_name"/></div>' + '<div><field name="turtle_foo"/></div>' + + // field without Widget in the list + '<div><field name="condition"/></div>' + '<div> <a type="edit"> Edit </a> </div>' + '</t>' + '</templates>' + @@ -2507,6 +2895,7 @@ QUnit.module('relational_fields', { '<form>' + '<field name="product_id" widget="statusbar"/>' + '<field name="model_name"/>' + + // field with Widget requiring specialData in the form '<field name="condition" widget="domain" options="{\'model\': \'model_name\'}"/>' + '</form>' + '</field>' + @@ -2714,6 +3103,63 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many with x2many in form view (but not in list view)', function (assert) { + assert.expect(1); + + // avoid error when saving the edited related record (because the + // related x2m field is unknown in the inline list view) + // also ensure that the changes are correctly saved + + this.data.turtle.fields.o2m = {string: "o2m", type: "one2many", relation: 'user'}; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<group>' + + '<field name="turtles">' + + '<tree>' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</group>' + + '</form>', + res_id: 1, + archs: { + "turtle,false,form": '<form string="Turtles">' + + '<field name="partner_ids" widget="many2many_tags"/>' + + '</form>', + }, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1].turtles, [[1, 2, { + partner_ids: [[6, false, [2, 4, 1]]], + }]]); + } + return this._super.apply(this, arguments); + }, + }); + + form.$('.o_data_row:first').click(); // edit first record + + var $input = $('.modal .o_field_many2manytags input'); + $input.click(); // opens the dropdown + $input.autocomplete('widget').find('li').click(); // add 'first record' + + // add a many2many tag and save + $('.modal .o_field_x2many_list_row_add a').click(); + $('.modal .o_field_widget[name=name]').val('test').trigger('input'); + $('.modal .modal-footer .btn-primary').click(); // save + + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) { assert.expect(1); @@ -3103,6 +3549,201 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many list editable: \'required\' modifiers is properly working', function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.int_field = obj.turtles.length; + }, + }; + + this.data.partner.records[0].turtles = []; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field"/>' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_foo" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10", + "int_field should start with value 10"); + + form.$('.o_field_x2many_list_row_add a').click(); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10", + "int_field should still be 10 (no onchange, because line is not valid)"); + + // fill turtle_foo field + form.$('.o_field_widget[name="turtle_foo"]').val("some text").trigger('input'); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1", + "int_field should be 1 (onchange triggered, because line is now valid)"); + + form.destroy(); + }); + + QUnit.test('one2many list editable: \'required\' modifiers is properly working, part 2', function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.int_field = obj.turtles.length; + }, + }; + + this.data.partner.records[0].turtles = []; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field"/>' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_int"/>' + + '<field name="turtle_foo" attrs=\'{"required": [["turtle_int", "=", 0]]}\'/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10", + "int_field should start with value 10"); + + form.$('.o_field_x2many_list_row_add a').click(); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10", + "int_field should still be 10 (no onchange, because line is not valid)"); + + // fill turtle_int field + form.$('.o_field_widget[name="turtle_int"]').val("1").trigger('input'); + + assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1", + "int_field should be 1 (onchange triggered, because line is now valid)"); + + form.destroy(); + }); + + QUnit.test('one2many list editable: add new line before onchange returns', function (assert) { + // If the user adds a new row (with a required field with onchange), selects + // a value for that field, then adds another row before the onchange returns, + // the editable list must wait for the onchange to return before trying to + // unselect the first row, otherwise it will be detected as invalid. + assert.expect(7); + + this.data.turtle.onchanges = { + turtle_trululu: function () {}, + }; + + var def; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom">' + + '<field name="turtle_trululu" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + // add a first line but hold the onchange back + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('.o_data_row').length, 1, + "should have created the first row immediately"); + def = $.Deferred(); + form.$('.o_field_many2one input').click(); + form.$('.o_field_many2one input').autocomplete('widget').find('a').first().click(); + + // try to add a second line and check that it is correctly waiting + // for the onchange to return + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual($('.modal').length, 0, "no modal should be displayed"); + assert.strictEqual($('.o_field_invalid').length, 0, + "no field should be marked as invalid"); + assert.strictEqual(form.$('.o_data_row').length, 1, + "should wait for the onchange to create the second row"); + assert.ok(form.$('.o_data_row').hasClass('o_selected_row'), + "first row should still be in edition"); + + // resolve the onchange def + def.resolve(); + assert.strictEqual(form.$('.o_data_row').length, 2, + "second row should now have been created"); + assert.notOk(form.$('.o_data_row:first').hasClass('o_selected_row'), + "first row should no more be in edition"); + + form.destroy(); + }); + + QUnit.test('editable list: multiple clicks on Add an item do not create invalid rows', function (assert) { + assert.expect(3); + + this.data.turtle.onchanges = { + turtle_trululu: function () {}, + }; + + var def; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom">' + + '<field name="turtle_trululu" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + // click twice to add a new line + def = $.Deferred(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('.o_data_row').length, 0, + "no row should have been created yet (waiting for the onchange)"); + + // resolve the onchange def + def.resolve(); + assert.strictEqual(form.$('.o_data_row').length, 1, + "only one row should have been created"); + assert.ok(form.$('.o_data_row:first').hasClass('o_selected_row'), + "the created row should be in edition"); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { @@ -3579,6 +4220,31 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2many list: list of id as default value', function (assert) { + assert.expect(1); + + this.data.partner.fields.turtles.default = [2, 3]; + this.data.partner.fields.turtles.type = "many2many"; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree>' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + }); + + assert.strictEqual(form.$('td.o_data_cell').text(), "blipkawa", + "should have loaded default data"); + + form.destroy(); + }); + QUnit.test('many2many list with x2many: add a record', function (assert) { assert.expect(18); @@ -3652,6 +4318,91 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2many with a domain', function (assert) { + // The domain specified on the field should not be replaced by the potential + // domain the user writes in the dialog, they should rather be concatenated + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="timmy" domain="[[\'display_name\', \'=\', \'gold\']]"/>' + + '</form>', + res_id: 1, + archs: { + 'partner_type,false,list': '<tree>' + + '<field name="display_name"/>' + + '</tree>', + 'partner_type,false,search': '<search>' + + '<field name="display_name" string="Name"/>' + + '</search>', + }, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual($('.modal .o_data_row').length, 1, + "should contain only one row (gold)"); + + $('.modal .o_searchview_input').trigger({type: 'keypress', which: 115}); // s + $('.modal .o_searchview_input').trigger({type: 'keydown', which: 13}); // enter + + assert.strictEqual($('.modal .o_data_row').length, 0, "should contain no row"); + + form.destroy(); + }); + + QUnit.test('many2many list with onchange and edition of a record', function (assert) { + assert.expect(7); + + this.data.partner.fields.turtles.type = "many2many"; + this.data.partner.onchanges.turtles = function () {}; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree>' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'turtle,false,form': '<form string="Turtle Power"><field name="turtle_bar"/></form>', + }, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('td.o_data_cell:first').click(); + + $('.modal-body input[type="checkbox"]').click(); + $('.modal .modal-footer .btn-primary').first().click(); + + // there is nothing left to save -> should not do a 'write' RPC + form.$buttons.find('.o_form_button_save').click(); + + assert.verifySteps([ + 'read', // read initial record (on partner) + 'read', // read many2many turtles + 'read', // read missing field when opening record in modal form view + 'write', // when saving the modal + 'onchange', // onchange should be triggered on partner + 'read', // reload many2many + ]); + + form.destroy(); + }); + QUnit.module('FieldStatus'); QUnit.test('static statusbar widget on many2one field', function (assert) { @@ -3979,6 +4730,65 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('unset selection field with 0 as key', function (assert) { + // The server doesn't make a distinction between false value (the field + // is unset), and selection 0, as in that case the value it returns is + // false. So the client must convert false to value 0 if it exists. + assert.expect(2); + + this.data.partner.fields.selection = { + type: "selection", + selection: [[0, "Value O"], [1, "Value 1"]], + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="selection"/>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').text(), 'Value O', + "the displayed value should be 'Value O'"); + assert.notOk(form.$('.o_field_widget').hasClass('o_field_empty'), + "should not have class o_field_empty"); + + form.destroy(); + }); + + QUnit.test('unset selection field with string keys', function (assert) { + // The server doesn't make a distinction between false value (the field + // is unset), and selection 0, as in that case the value it returns is + // false. So the client must convert false to value 0 if it exists. In + // this test, it doesn't exist as keys are strings. + assert.expect(2); + + this.data.partner.fields.selection = { + type: "selection", + selection: [['0', "Value O"], ['1', "Value 1"]], + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="selection"/>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').text(), '', + "there should be no displayed value"); + assert.ok(form.$('.o_field_widget').hasClass('o_field_empty'), + "should have class o_field_empty"); + + form.destroy(); + }); + QUnit.module('FieldMany2ManyTags'); QUnit.test('fieldmany2many tags: rendering and edition', function (assert) { @@ -4317,7 +5127,6 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('fieldradio widget on a selection in a new record', function (assert) { assert.expect(4); @@ -4345,6 +5154,55 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('fieldradio widget with numerical keys encoded as strings', function (assert) { + assert.expect(5); + + this.data.partner.fields.selection = { + type: 'selection', + selection: [['0', "Red"], ['1', "Black"]], + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="selection" widget="radio"/>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].selection, '1', + "should write correct value"); + } + return this._super.apply(this, arguments); + }, + }); + + + assert.strictEqual(form.$('.o_field_widget').text(), '', + "field should be unset"); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_radio_input:checked').length, 0, + "no value should be checked"); + + form.$("input.o_radio_input:nth(1)").click(); // click on 2nd option + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget').text(), 'Black', + "value should be 'Black'"); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_radio_input[data-index=1]:checked').length, 1, + "'Black' should be checked"); + + form.destroy(); + }); + QUnit.module('FieldMany2ManyCheckBoxes'); QUnit.test('widget many2many_checkboxes', function (assert) { @@ -4377,7 +5235,7 @@ QUnit.module('relational_fields', { assert.notOk(form.$('div.o_field_widget div.o_checkbox input').prop('disabled'), "the checkboxes should not be disabled"); - // add a m2m value + // add a m2m value by clicking on input form.$('div.o_field_widget div.o_checkbox input').eq(1).click(); form.$buttons.find('.o_form_button_save').click(); assert.deepEqual(this.data.partner.records[0].timmy, [12, 14], @@ -4385,9 +5243,9 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('input:checked').length, 2, "both checkboxes should be checked"); - // remove a m2m value + // remove a m2m value by clinking on label form.$buttons.find('.o_form_button_edit').click(); - form.$('div.o_field_widget div.o_checkbox input').eq(0).click(); + form.$('div.o_field_widget div.o_checkbox + label').eq(0).click(); form.$buttons.find('.o_form_button_save').click(); assert.deepEqual(this.data.partner.records[0].timmy, [14], "should have removed the first element to the many2many"); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index e7ee9bf07d2..1f4160a3e41 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -598,8 +598,11 @@ var MockServer = Class.extend({ var self = this; var fields = this.data[model].fields; var aggregatedFields = _.clone(kwargs.fields); - var groupByField = kwargs.groupby[0]; - var result = []; + var groupBy = []; + if (kwargs.groupby.length) { + groupBy = kwargs.lazy ? [kwargs.groupby[0]] : kwargs.groupby; + } + var records = this._getRecords(model, kwargs.domain); // if no fields have been given, the server picks all stored fields if (aggregatedFields.length === 0) { @@ -611,7 +614,6 @@ var MockServer = Class.extend({ return name in self.data[model].fields; }); - function aggregateFields(group, records) { var type; for (var i = 0; i < aggregatedFields.length; i++) { @@ -624,77 +626,80 @@ var MockServer = Class.extend({ } } } - - var records = this._getRecords(model, kwargs.domain); - if (groupByField) { - var originalGroupByField = groupByField; - var groupByFieldDescr = fields[groupByField.split(':')[0]]; - var groupByFunction, formatValue; - if (groupByFieldDescr.type === 'date') { - - var aggregateFunction = groupByField.split(':')[1] || 'month'; - - groupByField = groupByField.split(':')[0]; - groupByFunction = function (obj) { - if (aggregateFunction === 'day') { - return moment(obj[groupByField]).format('YYYY-MM-DD'); - } else { - return moment(obj[groupByField]).format('MMMM YYYY'); - } - }; - formatValue = function (val) { + function formatValue(groupByField, val) { + var fieldName = groupByField.split(':')[0]; + var aggregateFunction = groupByField.split(':')[1] || 'month'; + if (fields[fieldName].type === 'date') { + if (aggregateFunction === 'day') { + return moment(val).format('YYYY-MM-DD'); + } else { + return moment(val).format('MMMM YYYY'); + } + } else { + return val instanceof Array ? val[0] : (val || false); + } + } + function groupByFunction(record) { + var value = ''; + _.each(groupBy, function (groupByField) { + value = (value ? value + ',' : value) + groupByField + '#'; + var fieldName = groupByField.split(':')[0]; + if (fields[fieldName].type === 'date') { + var aggregateFunction = groupByField.split(':')[1] || 'month'; if (aggregateFunction === 'day') { - return moment(val).format('YYYY-MM-DD'); + value += moment(record[fieldName]).format('YYYY-MM-DD'); } else { - return moment(val).format('MMMM YYYY'); + value += moment(record[fieldName]).format('MMMM YYYY'); } - }; - } else { - groupByFunction = function (obj) { - return obj[groupByField]; - }; - formatValue = function (val) { - return val instanceof Array ? val[0] : (val || false); - }; - } - _.each(_.groupBy(records, groupByFunction), function (g, val) { - val = formatValue(g[0][groupByField]); - var group = { - __domain: [[ - groupByField, "=", - val instanceof Array ? val[0] : (val || false) - ]].concat(kwargs.domain || []), - }; - var field = self.data[model].fields[groupByField]; + } else { + value += record[groupByField]; + } + }); + return value; + } + + if (!groupBy.length) { + var group = { __count: records.length }; + aggregateFields(group, records); + return [group]; + } + + var groups = _.groupBy(records, groupByFunction); + var result = _.map(groups, function (group) { + var res = { + __domain: kwargs.domain || [], + }; + _.each(groupBy, function (groupByField) { + var fieldName = groupByField.split(':')[0]; + var val = formatValue(groupByField, group[0][fieldName]); + var field = self.data[model].fields[fieldName]; if (field.type === 'many2one' && !_.isArray(val)) { var related_record = _.findWhere(self.data[field.relation].records, { id: val }); if (related_record) { - group[originalGroupByField] = [val, related_record.display_name]; + res[groupByField] = [val, related_record.display_name]; } else { - group[originalGroupByField] = false; + res[groupByField] = false; } } else { - group[originalGroupByField] = val; - } - - // compute count key to match dumb server logic... - var countKey; - if (kwargs.lazy) { - countKey = groupByField + "_count"; - } else { - countKey = "__count"; + res[groupByField] = val; } - group[countKey] = g.length; - aggregateFields(group, g); - result.push($.extend(true, {}, group)); + res.__domain = [[fieldName, "=", val]].concat(res.__domain); }); - } else { - var group = { __count: records.length }; - aggregateFields(group, records); - result.push(group); - } + + // compute count key to match dumb server logic... + var countKey; + if (kwargs.lazy) { + countKey = groupBy[0].split(':')[0] + "_count"; + } else { + countKey = "__count"; + } + res[countKey] = group.length; + aggregateFields(res, group); + + return res; + }); return result; }, /** diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index 180d38668c7..cc3834e188e 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -707,7 +707,7 @@ QUnit.module('Views', { }); QUnit.test('internal state of groups remains when reloading', function (assert) { - assert.expect(9); + assert.expect(10); this.params.fieldNames = ['foo']; this.params.domain = []; @@ -739,6 +739,9 @@ QUnit.module('Views', { var record = model.get(resultID); assert.strictEqual(record.data.length, 2, "should have 2 groups"); var groupID = record.data[0].id; + assert.strictEqual(model.localData[groupID].parentID, resultID, + "parentID should be correctly set on groups"); + model.toggleGroup(groupID); record = model.get(resultID); @@ -772,6 +775,43 @@ QUnit.module('Views', { model.destroy(); }); + QUnit.test('read group when grouped by a selection field', function (assert) { + assert.expect(5); + + this.data.partner.fields.selection = { + type: 'selection', + selection: [['a', 'A'], ['b', 'B']], + }; + this.data.partner.records[0].selection = 'a'; + + var model = createModel({ + Model: BasicModel, + data: this.data, + }); + var params = { + modelName: 'partner', + fields: this.data.partner.fields, + fieldNames: ['foo'], + groupedBy: ['selection'], + }; + + model.load(params).then(function (resultID) { + var dataPoint = model.get(resultID); + assert.strictEqual(dataPoint.data.length, 2, "should have two groups"); + + var groupFalse = _.findWhere(dataPoint.data, {value: false}); + assert.ok(groupFalse, "should have a group for value false"); + assert.deepEqual(groupFalse.domain, [['selection', '=', false]], + "group's domain should be correct"); + + var groupA = _.findWhere(dataPoint.data, {value: 'A'}); + assert.ok(groupA, "should have a group for value 'a'"); + assert.deepEqual(groupA.domain, [['selection', '=', 'a']], + "group's domain should be correct"); + }); + model.destroy(); + }); + QUnit.test('create record, then save', function (assert) { assert.expect(5); @@ -1371,4 +1411,71 @@ QUnit.module('Views', { model.destroy(); }); + + QUnit.test('changes are discarded when reloading from a new record', function (assert) { + // practical use case: click on 'Create' to open a form view in edit + // mode (new record), click on 'Discard', then open an existing record + assert.expect(2); + + this.data.partner.fields.foo.default = 'default'; + var model = createModel({ + Model: BasicModel, + data: this.data, + }); + + // load a new record (default_get) + var params = _.extend(this.params, { + res_id: undefined, + type: 'record', + fieldNames: ['foo'], + }); + model.load(params).then(function (resultID) { + var record = model.get(resultID); + assert.strictEqual(record.data.foo, 'default', + "should be the default value"); + + // reload with id 2 + model.reload(record.id, {currentId: 2}).then(function (resultID) { + var record = model.get(resultID); + assert.strictEqual(record.data.foo, 'gnap', + "should be the value of record 2"); + }); + }); + + model.destroy(); + }); + + QUnit.test('has a proper evaluation context', function (assert) { + assert.expect(1); + + this.params.fieldNames = Object.keys(this.data.partner.fields); + this.params.res_id = 1; + + var model = createModel({ + Model: BasicModel, + data: this.data, + }); + + model.load(this.params).then(function (resultID) { + var record = model.get(resultID); + assert.deepEqual(record.evalContext, { + active_id: 1, + active_ids: [1], + active_model: "partner", + bar: 1, + category: [12], + current_date: moment().format('YYYY-MM-DD'), + date: "2017-01-25", + display_name: "first partner", + foo: "blip", + id: 1, + product_id: 37, + product_ids: [], + qux: false, + total: 0 + }, "should use the proper eval context"); + }); + model.destroy(); + }); + });}); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index ec41f21bfc1..45dbc1dd3b3 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -298,6 +298,33 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('properly handle modifiers and attributes on notebook tags', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<field name="product_id"/>' + + '<notebook class="new_class" attrs=\'{"invisible": [["product_id", "=", false]]}\'>' + + '<page string="Foo">' + + '<field name="foo"/>' + + '</page>' + + '</notebook>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.ok(form.$('.o_notebook').hasClass('o_invisible_modifier'), + 'the notebook should handle modifiers (invisible)'); + assert.ok(form.$('.o_notebook').hasClass('new_class'), + 'the notebook should handle attributes'); + form.destroy(); + }); + QUnit.test('invisible attrs on first notebook page', function (assert) { assert.expect(6); @@ -849,8 +876,9 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.strictEqual(event.data.action_data.name, "post", "should trigger execute_action with correct method name"); - assert.strictEqual(event.data.record_id, 2, "should have correct id in event data"); + assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data"); event.data.on_success(); + event.data.on_closed(); }); rpcCount = 0; form.$('.o_form_statusbar button.p').click(); @@ -862,7 +890,7 @@ QUnit.module('Views', { }); form.$('.o_form_statusbar button.s').click(); - assert.strictEqual(rpcCount, 2, "should have done 1 rpcs to reload"); + assert.strictEqual(rpcCount, 2, "should have done 2 rpcs to reload"); form.destroy(); }); @@ -901,9 +929,10 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.step('execute_action'); - assert.strictEqual(event.data.record_id, resID, + assert.deepEqual(event.data.res_ids, [resID], "execute action should be done on correct record id"); event.data.on_success(); + event.data.on_closed(); }); form.$('.o_form_statusbar button.p').click(); @@ -950,9 +979,10 @@ QUnit.module('Views', { testUtils.intercept(form, 'execute_action', function (event) { assert.step('execute_action'); - assert.strictEqual(event.data.record_id, resID, + assert.deepEqual(event.data.res_ids, [resID], "execute action should be done on correct record id"); event.data.on_success(); + event.data.on_closed(); }); form.$('.o_form_statusbar button.p').click(); @@ -1159,6 +1189,38 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('default record with a one2many and an onchange on sub field', function (assert) { + assert.expect(4); + + this.data.partner.onchanges.foo = function () {}; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p">' + + '<tree>' + + '<field name="foo"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (route, args) { + assert.step(args.method); + if (args.method === 'onchange') { + assert.deepEqual(args.args[3], { + p: '', + 'p.display_name': '', + 'p.foo': '1' + }, "onchangeSpec should be correct (with sub fields)"); + } + return this._super.apply(this, arguments); + }, + }); + assert.verifySteps(['default_get', 'onchange']); + form.destroy(); + }); + QUnit.test('sidebar is hidden when switching to edit mode', function (assert) { assert.expect(3); @@ -2248,7 +2310,6 @@ QUnit.module('Views', { "input should be tagged as invalid"); form.$('input').val("tralala").trigger('input'); - form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_field_invalid').length, 0, "nothing should be marked as invalid"); @@ -4157,8 +4218,9 @@ QUnit.module('Views', { }); QUnit.test('default_order on x2many embedded view', function (assert) { - assert.expect(1); + assert.expect(8); + this.data.partner.fields.display_name.sortable = true; this.data.partner.records[0].p = [1, 4]; var form = createView({ @@ -4175,11 +4237,46 @@ QUnit.module('Views', { '</field>' + '</sheet>' + '</form>', + archs: { + 'partner,false,form': + '<form string="Partner">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + }, res_id: 1, }); assert.ok(form.$('.o_field_one2many tbody tr:first td:contains(yop)').length, "record 1 should be first"); + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual($('.modal').length, 1, + "FormViewDialog should be opened"); + $('.modal input[name="foo"]').val('xop').trigger("input"); + $('.modal .modal-footer button:eq(1)').click(); // Save & new + $('.modal input[name="foo"]').val('zop').trigger("input"); + $('.modal .modal-footer button:first').click(); // Save & close + + // client-side sort + assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zop)').length, + "record zop should be first"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').length, + "record yop should be second"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length, + "record xop should be third"); + + // server-side sort + form.$buttons.find('.o_form_button_save').click(); + assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zop)').length, + "record zop should be first"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').length, + "record yop should be second"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length, + "record xop should be third"); form.destroy(); }); @@ -4457,5 +4554,305 @@ QUnit.module('Views', { _t.database.multi_lang = multi_lang; }); + + QUnit.test('buttons are disabled until action is resolved', function (assert) { + assert.expect(3); + + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<div name="button_box" class="oe_button_box">' + + '<button class="oe_stat_button">' + + '<field name="bar"/>' + + '</button>' + + '</div>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + intercepts: { + execute_action: function (event) { + return def.then(function() { + event.data.on_success(); + }); + } + }, + }); + + assert.notOk(form.$('.oe_button_box button').attr('disabled'), + "stat buttons should be enabled"); + + form.$('.oe_button_box button').click(); + + // The unresolved deferred lets us check the state of the buttons + assert.ok(form.$('.oe_button_box button').attr('disabled'), + "stat buttons should be disabled"); + + def.resolve(); + + assert.notOk(form.$('.oe_button_box button').attr('disabled'), + "stat buttons should be enabled"); + + form.destroy(); + }); + + QUnit.test('buttons are disabled until action is resolved (in dialogs)', function (assert) { + assert.expect(3); + + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<field name="trululu"/>' + + '</sheet>' + + '</form>', + archs: { + 'partner,false,form': '<form>' + + '<sheet>' + + '<div name="button_box" class="oe_button_box">' + + '<button class="oe_stat_button">' + + '<field name="bar"/>' + + '</button>' + + '</div>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + }, + res_id: 1, + intercepts: { + execute_action: function (event) { + return def.then(function() { + event.data.on_success(); + }); + } + }, + mockRPC: function (route, args) { + if (args.method === 'get_formview_id') { + return $.when(false); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('.o_external_button').click(); + + assert.notOk($('.modal .oe_button_box button').attr('disabled'), + "stat buttons should be enabled"); + + $('.modal .oe_button_box button').click(); + + assert.ok($('.modal .oe_button_box button').attr('disabled'), + "stat buttons should be disabled"); + + def.resolve(); + + assert.notOk($('.modal .oe_button_box button').attr('disabled'), + "stat buttons should be enabled"); + + form.destroy(); + }); + + QUnit.test('multiple clicks on save should reload only once', function (assert) { + assert.expect(4); + + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + assert.step(args.method); + if (args.method === "write") { + return def.then(function () { + return result; + }); + } else { + return result; + } + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="foo"]').val("test").trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + form.$buttons.find('.o_form_button_save').click(); + + def.resolve(); + + assert.verifySteps([ + 'read', // initial read to render the view + 'write', // write on save + 'read' // read on reload + ]); + + form.destroy(); + }); + + QUnit.test('form view is not broken if save operation fails', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + assert.step(args.method); + if (args.method === 'write' && args.args[1].foo === 'incorrect value') { + return $.Deferred().reject(); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="foo"]').val("incorrect value").trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + + form.$('input[name="foo"]').val("correct value").trigger("input"); + + form.$buttons.find('.o_form_button_save').click(); + + assert.verifySteps([ + 'read', // initial read to render the view + 'write', // write on save (it fails, does not trigger a read) + 'write', // write on save (it works) + 'read' // read on reload + ]); + + form.destroy(); + }); + + QUnit.test('support password attribute', function (assert) { + assert.expect(3); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo" password="True"/>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('span[name="foo"]').text(), '***', + "password should be displayed with stars"); + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('input[name="foo"]').val(), '***', + "password should be displayed with stars"); + assert.strictEqual(form.$('input[name="foo"]').prop('type'), 'password', + "input should be of type password"); + form.destroy(); + }); + + QUnit.test('support autocomplete attribute', function (assert) { + assert.expect(3); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="display_name" autocomplete="coucou"/>' + + '<field name="foo" password="True"/>' + + '</form>', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('input[name="foo"]').val(), '***', + "password should be displayed with stars"); + assert.strictEqual(form.$('input[name="display_name"]').prop('autocomplete'), 'coucou', + "attribute autocomplete should be set"); + assert.strictEqual(form.$('input[name="foo"]').prop('autocomplete'), 'new-password', + "attribute autocomplete should be set to 'new-password' on password input"); + form.destroy(); + }); + + QUnit.test('context is correctly passed after save & new in FormViewDialog', function (assert) { + assert.expect(3); + + var form = createView({ + View: FormView, + model: 'partner', + res_id: 4, + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="product_ids"/>' + + '</group>' + + '</sheet>' + + '</form>', + archs: { + 'product,false,form': + '<form string="Products">' + + '<sheet>' + + '<group>' + + '<field name="partner_type_id" ' + + 'context="{\'color\': parent.id}"/>' + + '</group>' + + '</sheet>' + + '</form>', + 'product,false,list': '<tree><field name="display_name"/></tree>' + }, + mockRPC: function (route, args) { + if (args.method === 'name_search') { + assert.strictEqual(args.kwargs.context.color, 4, + "should use the correct context"); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual($('.modal').length, 1, + "One FormViewDialog should be opened"); + // set a value on the m2o + var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); + $('.modal .o_field_many2one input').click(); + $dropdown.find('li:first()').click(); + + $('.modal .modal-footer button:eq(1)').click(); // Save & new + $('.modal .o_field_many2one input').click(); + $('.modal .modal-footer button:first').click(); // Save & close + form.destroy(); + }); + }); }); diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js index 7c64b7dcf6a..eeb07c17358 100644 --- a/addons/web/static/tests/views/kanban_model_tests.js +++ b/addons/web/static/tests/views/kanban_model_tests.js @@ -62,7 +62,7 @@ QUnit.module('Views', { QUnit.module('KanbanModel'); QUnit.test('load grouped + add a new group', function (assert) { - assert.expect(20); + assert.expect(22); var calledRoutes = {}; var model = createModel({ @@ -87,7 +87,8 @@ QUnit.module('Views', { // various checks on the load result var state = model.get(resultID); assert.ok(_.isEqual(state.groupedBy, ['product_id']), 'should be grouped by "product_id"'); - assert.strictEqual(state.count, 2, 'should have found 2 groups'); + assert.strictEqual(state.data.length, 2, 'should have found 2 groups'); + assert.strictEqual(state.count, 2, 'both groups contain one record'); var xphoneGroup = _.findWhere(state.data, {res_id: 37}); assert.strictEqual(xphoneGroup.model, 'partner', 'group should have correct model'); assert.ok(xphoneGroup, 'should have a group for res_id 37'); @@ -102,7 +103,8 @@ QUnit.module('Views', { // add a new group model.createGroup('xpod', resultID); state = model.get(resultID); - assert.strictEqual(state.count, 3, 'should now have 3 groups'); + assert.strictEqual(state.data.length, 3, 'should now have 3 groups'); + assert.strictEqual(state.count, 2, 'there are still 2 records'); var xpodGroup = _.findWhere(state.data, {value: 'xpod'}); assert.strictEqual(xpodGroup.model, 'partner', 'new group should have correct model'); assert.ok(xpodGroup, 'should have an "xpod" group'); @@ -246,7 +248,48 @@ QUnit.module('Views', { model.destroy(); done(); }); + }); + + QUnit.test('add record to group', function (assert) { + assert.expect(8); + + var self = this; + var model = createModel({ + Model: KanbanModel, + data: this.data, + }); + var params = _.extend(this.params, { + groupedBy: ['product_id'], + fieldNames: ['foo'], + }); + + model.load(params).then(function (stateID) { + self.data.partner.records.push({id: 3, foo: 'new record', product_id: 37}); + + var state = model.get(stateID); + assert.deepEqual(state.res_ids, [1, 2], + "state should have the correct res_ids"); + assert.strictEqual(state.count, 2, + "state should have the correct count"); + assert.strictEqual(state.data[0].count, 1, + "first group should contain one record"); + + return model.addRecordToGroup(state.data[0].id, 3).then(function () { + var state = model.get(stateID); + assert.deepEqual(state.res_ids, [3, 1, 2], + "state should have the correct res_ids"); + assert.strictEqual(state.count, 3, + "state should have the correct count"); + assert.deepEqual(state.data[0].res_ids, [3, 1], + "new record's id should have been added to the res_ids"); + assert.strictEqual(state.data[0].count, 2, + "first group should now contain two records"); + assert.strictEqual(state.data[0].data[0].data.foo, 'new record', + "new record should have been fetched"); + }); + }); + model.destroy(); }); }); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 7f853138265..b2b84ff00d5 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -323,8 +323,9 @@ QUnit.module('Views', { }); QUnit.test('quick create in grouped mode', function (assert) { - assert.expect(4); + assert.expect(6); + var nbRecords = 4; var kanban = createView({ View: KanbanView, model: 'partner', @@ -335,6 +336,12 @@ QUnit.module('Views', { '<div><field name="foo"/></div>' + '</t></templates></kanban>', groupBy: ['bar'], + intercepts: { + env_updated: function (event) { + assert.strictEqual(event.data.ids.length, nbRecords, + "should update the env with the records ids"); + }, + } }); // click to add an element and cancel the quick creation @@ -351,6 +358,7 @@ QUnit.module('Views', { kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); $quickCreate = kanban.$('.o_kanban_quick_create'); $quickCreate.find('input').val('new partner'); + nbRecords = 5; $quickCreate.find('button.o_kanban_add').click(); assert.strictEqual(this.data.partner.records.length, 5, @@ -682,7 +690,7 @@ QUnit.module('Views', { assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column)').length, 1, "the last column should be the newly created one"); kanban.destroy(); - }), + }); QUnit.test('delete a column in grouped on m2o', function (assert) { assert.expect(26); @@ -758,7 +766,7 @@ QUnit.module('Views', { assert.strictEqual(kanban.renderer.widgets.length, 2, "the old widgets should have been correctly deleted"); kanban.destroy(); - }), + }); QUnit.test('create a column, delete it and create another one', function (assert) { assert.expect(5); @@ -797,7 +805,7 @@ QUnit.module('Views', { assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column 2)').length, 1, "the last column should be the newly created one"); kanban.destroy(); - }), + }); QUnit.test('edit a column in grouped on m2o', function (assert) { assert.expect(12); diff --git a/addons/web/static/tests/views/list_benchmarks.js b/addons/web/static/tests/views/list_benchmarks.js index 7b4e1f7e0c7..4dadbcb47bd 100644 --- a/addons/web/static/tests/views/list_benchmarks.js +++ b/addons/web/static/tests/views/list_benchmarks.js @@ -23,7 +23,7 @@ QUnit.module('List View', { }, }; this.arch = null; - this.run = function (assert, done) { + this.run = function (assert, done, cb) { var data = this.data; var arch = this.arch; new Benchmark.Suite({}) @@ -34,6 +34,9 @@ QUnit.module('List View', { data: data, arch: arch, }); + if (cb) { + cb(list); + } list.destroy(); }) .on('cycle', function(event) { @@ -82,7 +85,7 @@ QUnit.module('List View', { this.run(assert, done); }); - QUnit.test('simple readonly list with 200 rows 4 fields', function (assert) { + QUnit.test('editable list with 200 rows 4 fields', function (assert) { var done = assert.async(); assert.expect(1); @@ -94,8 +97,16 @@ QUnit.module('List View', { bar: i % 2 === 0, }); } - this.arch = '<tree><field name="foo"/><field name="int_field"/><field name="bar"/><field name="qux"/></tree>'; - this.run(assert, done); + this.arch = '<tree editable="bottom">' + + '<field name="foo" attrs="{\'readonly\': [[\'bar\', \'=\', True]]}"/>' + + '<field name="int_field"/>' + + '<field name="bar"/>' + + '<field name="qux"/>' + + '</tree>'; + this.run(assert, done, function (list) { + list.$buttons.find('.o_list_button_add').click(); + list.$buttons.find('.o_list_button_discard').click(); + }); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index a74192731ec..0418158465b 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -843,6 +843,30 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('support row decoration (with unset numeric values)', function (assert) { + assert.expect(2); + + this.data.foo.records = []; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="bottom" decoration-danger="int_field < 0">' + + '<field name="int_field"/>' + + '</tree>', + }); + + list.$buttons.find('.o_list_button_add').click(); + + assert.strictEqual(list.$('tr.o_data_row.text-danger').length, 0, + "the data row should not have .text-danger decoration (int_field is unset)"); + list.$('input[name="int_field"]').val('-3').trigger('input'); + assert.strictEqual(list.$('tr.o_data_row.text-danger').length, 1, + "the data row should have .text-danger decoration (int_field is negative)"); + list.destroy(); + }); + QUnit.test('support row decoration with date', function (assert) { assert.expect(3); @@ -1087,7 +1111,7 @@ QUnit.module('Views', { }, intercepts: { execute_action: function (event) { - assert.strictEqual(event.data.record_id, 1, + assert.deepEqual(event.data.res_ids, [1], 'should call with correct id'); assert.strictEqual(event.data.model, 'foo', 'should call with correct model'); @@ -1432,7 +1456,7 @@ QUnit.module('Views', { }); QUnit.test('edition: create new line, then discard', function (assert) { - assert.expect(2); + assert.expect(8); var list = createView({ View: ListView, @@ -1443,10 +1467,22 @@ QUnit.module('Views', { assert.strictEqual(list.$('tr.o_data_row').length, 4, "should have 4 records"); + assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 1, + "create button should be visible"); + assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0, + "discard button should be hidden"); list.$buttons.find('.o_list_button_add').click(); + 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"); list.$buttons.find('.o_list_button_discard').click(); assert.strictEqual(list.$('tr.o_data_row').length, 4, "should still have 4 records"); + assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 1, + "create button should be visible again"); + assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0, + "discard button should be hidden again"); list.destroy(); }); @@ -2166,6 +2202,103 @@ QUnit.module('Views', { list.destroy(); }); + + QUnit.test('field values are escaped', function (assert) { + assert.expect(1); + var value = '<script>throw Error();</script>'; + + this.data.foo.records[0].foo = value; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top"><field name="foo"/></tree>', + }); + + assert.strictEqual(list.$('.o_data_cell:first').text(), value, + "value should have been escaped"); + + list.destroy(); + }); + + QUnit.test('pressing ESC discard the current line changes', function (assert) { + assert.expect(3); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top"><field name="foo"/></tree>', + }); + + list.$buttons.find('.o_list_button_add').click(); + + list.$('input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); + assert.strictEqual(list.$('tr.o_data_row').length, 4, + "should have 4 data row in list"); + assert.strictEqual(list.$('tr.o_data_row.o_selected_row').length, 0, + "no rows should be selected"); + assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'), + "should not have a visible save button"); + list.destroy(); + }); + + QUnit.test('field with password attribute', function (assert) { + assert.expect(2); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree><field name="foo" password="True"/></tree>', + }); + + assert.strictEqual(list.$('td.o_data_cell:eq(0)').text(), '***', + "should display string as password"); + assert.strictEqual(list.$('td.o_data_cell:eq(1)').text(), '****', + "should display string as password"); + + list.destroy(); + }); + + QUnit.test('multiple clicks on Add do not create invalid rows', function (assert) { + assert.expect(2); + + this.data.foo.onchanges = { + m2o: function () {}, + }; + + var def = $.Deferred(); + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top"><field name="m2o" required="1"/></tree>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + assert.strictEqual(list.$('.o_data_row').length, 4, + "should contain 4 records"); + + // click on Add twice, and delay the onchange + list.$buttons.find('.o_list_button_add').click(); + list.$buttons.find('.o_list_button_add').click(); + + def.resolve(); + + assert.strictEqual(list.$('.o_data_row').length, 5, + "only one record should have been created"); + + list.destroy(); + }); + }); }); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index df7d4b9f7d8..d74e29204d7 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -551,11 +551,9 @@ QUnit.module('Views', { }); QUnit.test('can expand all rows', function (assert) { - // FIXME: This test depends on an incomplete behavior of the mock server - // and will need to be updated when read_group can group on - // multiple field levels. - assert.expect(4); + assert.expect(7); + var nbReadGroups = 0; var pivot = createView({ View: PivotView, model: "partner", @@ -564,29 +562,191 @@ QUnit.module('Views', { '<field name="foo" type="measure"/>' + '<field name="product_id" type="row"/>' + '</pivot>', + mockRPC: function (route, args) { + if (args.method === 'read_group') { + nbReadGroups++; + } + return this._super.apply(this, arguments); + }, }); + assert.strictEqual(nbReadGroups, 2, "should have done 2 read_group RPCS"); assert.strictEqual(pivot.$('td.o_pivot_cell_value').text(), "321220", - "should have proper values in cells (total, result 1, result 2"); + "should have proper values in cells (total, result 1, result 2)"); // expand on date:days, product - pivot.update({groupBy: ['date:days', 'product']}); + nbReadGroups = 0; + pivot.update({groupBy: ['date:days', 'product_id']}); - assert.strictEqual(pivot.$('tbody tr').length, 7, - "should have 7 rows (total + 2 for each category)"); + assert.strictEqual(nbReadGroups, 3, "should have done 3 read_group RPCS"); + assert.strictEqual(pivot.$('tbody tr').length, 8, + "should have 7 rows (total + 3 for December and 2 for October and April)"); // collapse the last two rows pivot.$('.o_pivot_header_cell_opened').last().click(); pivot.$('.o_pivot_header_cell_opened').last().click(); - assert.strictEqual(pivot.$('tbody tr').length, 5, - "should have 5 rows now"); + assert.strictEqual(pivot.$('tbody tr').length, 6, + "should have 6 rows now"); // expand all - $('.o_pivot_expand_button').click(); + nbReadGroups = 0; + pivot.$buttons.find('.o_pivot_expand_button').click(); - assert.strictEqual(pivot.$('tbody tr').length, 7, - "should have 7 rows again"); + assert.strictEqual(nbReadGroups, 3, "should have done 3 read_group RPCS"); + assert.strictEqual(pivot.$('tbody tr').length, 8, + "should have 8 rows again"); + + pivot.destroy(); + }); + + QUnit.test('expand all with a delay', function (assert) { + assert.expect(3); + + var def; + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="foo" type="measure"/>' + + '<field name="product_id" type="row"/>' + + '</pivot>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'read_group') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + // expand on date:days, product + pivot.update({groupBy: ['date:days', 'product_id']}); + + assert.strictEqual(pivot.$('tbody tr').length, 8, + "should have 7 rows (total + 3 for December and 2 for October and April)"); + + // collapse the last two rows + pivot.$('.o_pivot_header_cell_opened').last().click(); + pivot.$('.o_pivot_header_cell_opened').last().click(); + + assert.strictEqual(pivot.$('tbody tr').length, 6, + "should have 6 rows now"); + + // expand all + def = $.Deferred(); + pivot.$buttons.find('.o_pivot_expand_button').click(); + def.resolve(); + + assert.strictEqual(pivot.$('tbody tr').length, 8, + "should have 8 rows again"); + + pivot.destroy(); + }); + + QUnit.test('can download a file', function (assert) { + assert.expect(1); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="date" interval="month" type="col"/>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + session: { + get_file: function (args) { + assert.strictEqual(args.url, '/web/pivot/export_xls', + "should call get_file with correct parameters"); + args.complete(); + }, + }, + }); + + $('.o_pivot_download').click(); + pivot.destroy(); + }); + + QUnit.test('getContext correctly returns measures and groupbys', function (assert) { + assert.expect(3); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="date" interval="day" type="col"/>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + }); + + assert.deepEqual(pivot.getContext(), { + pivot_column_groupby: ['date:day'], + pivot_measures: ['foo'], + pivot_row_groupby: [], + }, "context should be correct"); + + // expand header on field customer + pivot.$('thead .o_pivot_header_cell_closed:nth(1)').click(); + pivot.$('ul.o_pivot_field_menu > li[data-field="customer"] a').click(); + assert.deepEqual(pivot.getContext(), { + pivot_column_groupby: ['date:day', 'customer'], + pivot_measures: ['foo'], + pivot_row_groupby: [], + }, "context should be correct"); + + // expand row on field product_id + pivot.$('tbody .o_pivot_header_cell_closed').first().click(); + pivot.$('ul.o_pivot_field_menu > li[data-field="product_id"] a').click(); + assert.deepEqual(pivot.getContext(), { + pivot_column_groupby: ['date:day', 'customer'], + pivot_measures: ['foo'], + pivot_row_groupby: ['product_id'], + }, "context should be correct"); + + pivot.destroy(); + }); + + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { + assert.expect(7); + + this.data.partner.fields.amount = {string: "Amount", type: "float"}; + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="date" interval="day" type="col"/>' + + '<field name="amount" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + "column: should have one opened header"); + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(First)').length, 1, + "column: should display one closed header with 'First'"); + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(Second)').length, 1, + "column: should display one closed header with 'Second'"); + + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + "row: should have one opened header"); + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xphone)').length, 1, + "row: should display one closed header with 'xphone'"); + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xpad)').length, 1, + "row: should display one closed header with 'xpad'"); + + assert.strictEqual(pivot.$('tbody tr:first td:nth(3)').text(), '32', + "selected measure should be foo, with total 32"); pivot.destroy(); }); diff --git a/addons/web/static/tests/widgets/data_export_tests.js b/addons/web/static/tests/widgets/data_export_tests.js new file mode 100644 index 00000000000..83169a64e1f --- /dev/null +++ b/addons/web/static/tests/widgets/data_export_tests.js @@ -0,0 +1,132 @@ +odoo.define('web.data_export_tests', function (require) { +"use strict"; + +var framework = require('web.framework'); +var ListView = require('web.ListView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('widgets', { + beforeEach: function () { + this.data = { + partner: { + fields: { + foo: {string: "Foo", type: "char"}, + }, + records: [ + { + id: 1, + foo: "yop", + }, + ] + }, + 'ir.exports': { + fields: { + name: {string: "Name", type: "char"}, + }, + records: [], + }, + }; + } +}, function () { + + QUnit.module('Data Export'); + + + QUnit.test('exporting all data in list view', function (assert) { + assert.expect(7); + + var blockUI = framework.blockUI; + var unblockUI = framework.unblockUI; + framework.blockUI = function () { + assert.step('block UI'); + }; + framework.unblockUI = function () { + assert.step('unblock UI'); + }; + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="foo"/></tree>', + viewOptions: { + sidebar: true, + }, + mockRPC: function (route) { + if (route === '/web/export/formats') { + return $.when([ + {tag: 'csv', label: 'CSV'}, + {tag: 'xls', label: 'Excel'}, + ]); + } + if (route === '/web/export/get_fields') { + return $.when([ + { + field_type: "one2many", + string: "Activities", + required: false, + value: "activity_ids/id", + id: "activity_ids", + params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"}, + relation_field: "res_id", + children: true, + }, { + children: false, + field_type: 'text', + id: "note", + relation_field: null, + required: false, + string: 'Description', + value: "note", + } + ]); + } + return this._super.apply(this, arguments); + }, + session: { + get_file: function (params) { + assert.step(params.url); + params.complete(); + }, + }, + }); + + list.getParent().searchview = { + build_search_data: function () { + assert.step('build_search_data'); + return { + contexts: [], + domains: [], + groupbys: [], + }; + }, + }; + list.$('thead th.o_list_record_selector input').click(); + list.sidebar.$('a:contains(Export)').click(); + + assert.strictEqual($('.modal').length, 1, "a modal dialog should be open"); + assert.strictEqual($('span.o_tree_column:contains(Activities)').length, 1, + "the Activities field should be in the list of exportable fields"); + + // select the field Description, click on add, then export and close + $('.modal span:contains(Description)').click(); + $('.modal .o_add_field').click(); + $('.modal span:contains(Export To File)').click(); + $('.modal span:contains(Close)').click(); + + list.destroy(); + framework.blockUI = blockUI; + framework.unblockUI = unblockUI; + assert.verifySteps([ + 'build_search_data', + 'block UI', + '/web/export/csv', + 'unblock UI', + ]); + }); + +}); + +}); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index dee21aa3129..f15504ea140 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -458,6 +458,7 @@ <script type="text/javascript" src="/web/static/tests/chrome/search_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/pager_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/widgets/data_export_tests.js"/> <script type="text/javascript" src="/web/static/tests/widgets/domain_selector_tests.js"/> <script type="text/javascript" src="/web/static/tests/widgets/model_field_selector_tests.js"/> diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 00faf80c70f..c12ab41b4bb 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -3,6 +3,7 @@ odoo.define('web_editor.backend', function (require) { var AbstractField = require('web.AbstractField'); var basic_fields = require('web.basic_fields'); +var config = require('web.config'); var core = require('web.core'); var session = require('web.session'); var field_registry = require('web.field_registry'); @@ -23,12 +24,28 @@ var QWeb = core.qweb; */ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({ className: 'oe_form_field oe_form_field_html_text', + supportedFieldTypes: ['html'], //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** + * Summernote doesn't notify for changes done in code mode. We override + * commitChanges to manually switch back to normal mode before committing + * changes, so that the widget is aware of the changes done in code mode. + * + * @override + */ + commitChanges: function () { + // switch to WYSIWYG mode if currently in code mode to get all changes + if (config.debug && this.mode === 'edit') { + var layoutInfo = this.$textarea.data('layoutInfo'); + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false); + } + this._super.apply(this, arguments); + }, + /** * @override */ isSet: function () { @@ -60,7 +77,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({ * @returns {Object} the summernote configuration */ _getSummernoteConfig: function () { - var config = { + var summernoteConfig = { focus: false, height: 180, toolbar: [ @@ -79,10 +96,10 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({ lang: "odoo", onChange: this._doDebouncedAction.bind(this), }; - if (this.getSession().debug) { - config.toolbar.splice(7, 0, ['view', ['codeview']]); + if (config.debug) { + summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]); } - return config; + return summernoteConfig; }, /** * @override diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 60c096603d8..4ab1e7bed06 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -511,7 +511,12 @@ function prettify_html(html) { level--; } - result += token.trim().replace(/\s+/, ' '); + // don't trim inline content (which could change appearance) + if (!inline) { + token = token.trim(); + } + + result += token.replace(/\s+/, ' '); if (inline_level > level) { result += '\n'; diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js index 20a96a91e84..42f2103f95f 100644 --- a/addons/web_editor/static/src/js/snippets.editor.js +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -107,7 +107,7 @@ data.Class = Widget.extend({ return $from.closest(selector, parentNode); }, all: function ($from) { - return $from ? $from.find(selector) : $(selector); + return $from ? cssFind($from, selector) : $(selector); }, is: function ($from) { return $from.is(selector); @@ -130,15 +130,31 @@ data.Class = Widget.extend({ }); }, all: is_children ? function ($from) { - return ($from || self.$editable).find(selector); + return cssFind($from || self.$editable, selector); } : function ($from) { - return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector)); + $from = $from || self.$editable; + return $from.filter(selector).add(cssFind($from, selector)); }, is: function ($from) { return $from.is(selector); } }; } + + /** + * jQuery find function behavior is: + * $('A').find('A B') <=> $('A A B') + * The searches behavior to find options' DOM needs to be + * $('A').find('A B') <=> $('A B') + * This is what this function does. + * + * @param {jQuery} $from - the jQuery element(s) from which to search + * @param {string} selector - the CSS selector to match + * @returns {jQuery} + */ + function cssFind($from, selector) { + return $from.find('*').filter(selector); + } }, fetch_snippet_templates: function () { diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js index 05068972532..6b42e028b3a 100644 --- a/addons/web_editor/static/src/js/transcoder.js +++ b/addons/web_editor/static/src/js/transcoder.js @@ -121,6 +121,16 @@ var getMatchedCSSRules = function (a) { } }); + // text-decoration rule is decomposed in -line, -color and -style. This is + // however not supported by many browser/mail clients and the editor does + // not allow to change -color and -style rule anyway + if (style['text-decoration-line']) { + style['text-decoration'] = style['text-decoration-line']; + delete style['text-decoration-line']; + delete style['text-decoration-color']; + delete style['text-decoration-style']; + } + return style; }; diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js index 6a18566af2b..a053ad99043 100644 --- a/addons/web_editor/static/tests/web_editor_tests.js +++ b/addons/web_editor/static/tests/web_editor_tests.js @@ -121,4 +121,31 @@ QUnit.test('field html_frame widget', function (assert) { form.destroy(); }); +QUnit.test('field htmlsimple does not crash when commitChanges is called in mode=readonly', function (assert) { + assert.expect(1); + + var form = testUtils.createView({ + View: FormView, + model: 'mass.mailing', + data: this.data, + arch: '<form string="Partners">' + + '<header>' + + '<button name="some_method" class="s" string="Do it" type="object"/>' + + '</header>' + + '<sheet>' + + '<field name="body"/>' + + '</sheet>' + + '</form>', + res_id: 1, + intercepts: { + execute_action: function () { + assert.step('execute_action'); + } + }, + }); + + form.$('button:contains(Do it)').click(); + form.destroy(); +}); + }); diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index a97cebe76d5..0c0497ef843 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -175,8 +175,8 @@ class Http(models.AbstractModel): nearest_lang = not func and cls.get_nearest_lang(path[1]) url_lang = nearest_lang and path[1] preferred_lang = ((cook_lang if cook_lang in langs else False) - or (not is_a_bot and cls.get_nearest_lang(request.lang)) - or request.website.default_lang_code) + or request.website.default_lang_code + or (not is_a_bot and cls.get_nearest_lang(request.lang))) request.lang = context['lang'] = nearest_lang or preferred_lang # if lang in url but not the displayed or default language --> change or remove diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js index fc3ec4c4e29..219728aa552 100644 --- a/addons/website/static/src/js/website.seo.js +++ b/addons/website/static/src/js/website.seo.js @@ -555,8 +555,9 @@ var Configurator = Dialog.extend({ website.TopBar.include({ start: function () { + var self = this; this.$el.on('click', 'a[data-action=promote-current-page]', function () { - new Configurator(this).open(); + new Configurator(self).open(); }); return this._super(); } diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less index 5f96bbac0e6..cde2501e605 100644 --- a/addons/website/static/src/less/website.snippets.less +++ b/addons/website/static/src/less/website.snippets.less @@ -391,6 +391,7 @@ div.carousel { // Parallax Theme .parallax { position: relative; + z-index: 0; .oe_structure { > :first-child { diff --git a/addons/website/static/src/less/website.ui.less b/addons/website/static/src/less/website.ui.less index 8f4732024b4..b515ad40096 100644 --- a/addons/website/static/src/less/website.ui.less +++ b/addons/website/static/src/less/website.ui.less @@ -47,4 +47,10 @@ body.o_connected_user { font-size: 20px; } } + @media only screen and (max-width: @screen-xs-max) { + #oe_applications { + position: inherit; + z-index: 1002; + } + } } diff --git a/addons/website_blog/static/src/js/contentshare.js b/addons/website_blog/static/src/js/contentshare.js index 47cd88fc4a7..cfc02470f7f 100644 --- a/addons/website_blog/static/src/js/contentshare.js +++ b/addons/website_blog/static/src/js/contentshare.js @@ -22,9 +22,12 @@ popover_content += '<a class="o_share_comment mr12"><i class="fa fa-comment fa-lg mr4 ml4"/></a>'; } if($('.blog_title, .blog_content').hasClass('js_tweet')){ - var current_url = window.location.href; - var tweet_selected_text = this.getSelection('string').substring(0,option.maxLength-(current_url.length+option.author_name.length+7)); - var text = encodeURIComponent(_.str.sprintf('\"%s\" --@%s %s', tweet_selected_text, option.author_name, current_url)); + var tweet = '"%s" - %s'; + var baseLength = tweet.replace(/%s/g, '').length; + // Shorten the selected text to match the tweet max length + // Note: all (non-localhost) urls in a tweet have 23 characters https://support.twitter.com/articles/78124 + var selectedText = this.getSelection('string').substring(0, option.maxLength - baseLength - 23); + var text = encodeURIComponent(_.str.sprintf(tweet, selectedText, window.location.href)); popover_content += '<a onclick="window.open(\''+option.shareLink+text+'\',\'_'+option.target+'\',\'location=yes,height=570,width=520,scrollbars=yes,status=yes\')"><i class="ml4 mr4 fa fa-twitter fa-lg"/></a>'; } return popover_content; diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js index 5c2d82f7646..7985e0ebecc 100644 --- a/addons/website_blog/static/src/js/website_blog.js +++ b/addons/website_blog/static/src/js/website_blog.js @@ -58,7 +58,7 @@ function shareArticle(event){ $(document).ready(function() { if ($('.website_blog').length) { - $(".js_tweet, .js_comment").share({'author_name': $('#blog_author').text()}); + $(".js_tweet, .js_comment").share({}); $('.cover_footer').on('click', page_transist); $('a[href^="#blog_content"]').on('click', animate); $('.o_twitter, .o_facebook, .o_linkedin, .o_google, .o_twitter_complete, .o_facebook_complete, .o_linkedin_complete, .o_google_complete').on('click', shareArticle); diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 4946c5b25de..d6b10b37dc1 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -15,47 +15,63 @@ class WebsiteCustomer(http.Controller): @http.route([ '/customers', '/customers/page/<int:page>', - '/customers/country/<int:country_id>', - '/customers/country/<country_name>-<int:country_id>', - '/customers/country/<int:country_id>/page/<int:page>', - '/customers/country/<country_name>-<int:country_id>/page/<int:page>', - '/customers/tag/<tag_id>', - '/customers/tag/<tag_id>/page/<int:page>', - '/customers/tag/<tag_id>/country/<int:country_id>', - '/customers/tag/<tag_id>/country/<country_name>-<int:country_id>', - '/customers/tag/<tag_id>/country/<int:country_id>/page/<int:page>', - '/customers/tag/<tag_id>/country/<country_name>-<int:country_id>/page/<int:page>', + '/customers/country/<model("res.country"):country>', + '/customers/country/<model("res.country"):country>/page/<int:page>', + '/customers/industry/<model("res.partner.industry"):industry>', + '/customers/industry/<model("res.partner.industry"):industry>/page/<int:page>', + '/customers/industry/<model("res.partner.industry"):industry>/country/<model("res.country"):country>', + '/customers/industry/<model("res.partner.industry"):industry>/country/<model("res.country"):country>/page/<int:page>', ], type='http', auth="public", website=True) - def customers(self, country_id=0, page=0, country_name='', tag_id=0, **post): - Country = request.env['res.country'] + def customers(self, country=None, industry=None, page=0, **post): Tag = request.env['res.partner.tag'] Partner = request.env['res.partner'] - partner_name = post.get('search', '') + search_value = post.get('search') domain = [('website_published', '=', True), ('assigned_partner_id', '!=', False)] - if partner_name: + if search_value: domain += [ - '|', - ('name', 'ilike', post.get("search")), - ('website_description', 'ilike', post.get("search")) + '|', '|', + ('name', 'ilike', search_value), + ('website_description', 'ilike', search_value), + ('industry_id.name', 'ilike', search_value), ] + tag_id = post.get('tag_id') if tag_id: tag_id = unslug(tag_id)[1] or 0 domain += [('website_tag_ids', 'in', tag_id)] + # group by industry, based on customers found with the search(domain) + industries = Partner.sudo().read_group(domain, ["id", "industry_id"], groupby="industry_id", orderby="industry_id") + partners_count = Partner.sudo().search_count(domain) + + if industry: + domain.append(('industry_id', '=', industry.id)) + if industry.id not in (x['industry_id'][0] for x in industries if x['industry_id']): + if industry.exists(): + industries.append({ + 'industry_id_count': 0, + 'industry_id': (industry.id, industry.name) + }) + + industries.sort(key=lambda d: (d.get('industry_id') or (0, ''))[1]) + + industries.insert(0, { + 'industry_id_count': partners_count, + 'industry_id': (0, _("All Sectors of Activity")) + }) + # group by country, based on customers found with the search(domain) countries = Partner.sudo().read_group(domain, ["id", "country_id"], groupby="country_id", orderby="country_id") country_count = Partner.sudo().search_count(domain) - if country_id: - domain += [('country_id', '=', country_id)] - curr_country = Country.browse(country_id) - if country_id not in (x['country_id'][0] for x in countries if x['country_id']): - if curr_country.exists(): + if country: + domain += [('country_id', '=', country.id)] + if country.id not in (x['country_id'][0] for x in countries if x['country_id']): + if country.exists(): countries.append({ 'country_id_count': 0, - 'country_id': (curr_country.id, curr_country.name) + 'country_id': (country.id, country.name) }) countries.sort(key=lambda d: d['country_id'] and d['country_id'][1]) @@ -69,8 +85,10 @@ class WebsiteCustomer(http.Controller): # pager url = '/customers' - if country_id: - url += '/country/%s' % country_id + if industry: + url += '/industry/%s' % industry.id + if country: + url += '/country/%s' % country.id pager = request.website.pager( url=url, total=partner_count, page=page, step=self._references_per_page, scope=7, url_args=post @@ -85,8 +103,11 @@ class WebsiteCustomer(http.Controller): values = { 'countries': countries, - 'current_country_id': country_id or 0, - 'current_country': curr_country if country_id else False, + 'current_country_id': country.id if country else 0, + 'current_country': country or False, + 'industries': industries, + 'current_industry_id': industry.id if industry else 0, + 'current_industry': industry or False, 'partners': partners, 'google_map_partner_ids': google_map_partner_ids, 'pager': pager, diff --git a/addons/website_customer/views/website_customer_templates.xml b/addons/website_customer/views/website_customer_templates.xml index d4c743cdcd5..4380180277d 100644 --- a/addons/website_customer/views/website_customer_templates.xml +++ b/addons/website_customer/views/website_customer_templates.xml @@ -50,6 +50,9 @@ <a class="media-heading" t-attf-href="/customers/#{slug(partner)}"> <span t-field="partner.display_name"/> </a> + <t t-if="partner.industry_id"> + <a class="label label-default" t-attf-href="/customers/#{ 'industry/%s/' % slug(partner.industry_id) }#{ current_country_id and 'country/%s' % slug(current_country) or '' }" t-esc="partner.industry_id.name"/> + </t> <div t-field="partner.website_short_description"/> </div> </div> @@ -91,15 +94,32 @@ </xpath> </template> +<template id="opt_industry_list" inherit_id="website_customer.index" customize_show="True" name="Filter on Sector of Activity" priority="20"> + <xpath expr="//div[@id='ref_left_column']" position="inside"> + <h3>References by Sectors of Activity</h3> + <ul class="nav nav-pills nav-stacked mt16 mb32"> + <t t-foreach="industries" t-as="industry_dict"> + <t t-if="industry_dict['industry_id']"> + <li t-att-class="industry_dict['industry_id'][0] == current_industry_id and 'active' or None"> + <a t-attf-href="/customers/#{ industry_dict['industry_id'][0] and 'industry/%s/' % slug(industry_dict['industry_id']) or '' }#{ current_country_id and 'country/%s' % current_country_id or '' }#{ search_path }"> + <span class="badge pull-right" t-esc="industry_dict['industry_id_count'] or '0'"/> + <t t-esc="industry_dict['industry_id'][1]"/> + </a> + </li> + </t> + </t> + </ul> + </xpath> +</template> -<template id="opt_country_list" inherit_id="website_customer.index" customize_show="True" name="Filter on Countries"> +<template id="opt_country_list" inherit_id="website_customer.index" customize_show="True" name="Filter on Countries" priority="30"> <xpath expr="//div[@id='ref_left_column']" position="inside"> <h3>References by Country</h3> <ul class="nav nav-pills nav-stacked mt16 mb32"> <t t-foreach="countries" t-as="country_dict"> <t t-if="country_dict['country_id']"> <li t-att-class="country_dict['country_id'][0] == current_country_id and 'active' or None"> - <a t-attf-href="/customers/#{ tag and 'tag/%s/' % slug(tag) or '' }#{ country_dict['country_id'][0] and 'country/%s' % slug(country_dict['country_id']) or '' }#{ search_path }"> + <a t-attf-href="/customers/#{ current_industry_id and 'industry/%s/' % slug(current_industry) or '' }#{ country_dict['country_id'][0] and 'country/%s' % slug(country_dict['country_id']) or '' }#{ search_path }"> <span class="badge pull-right" t-esc="country_dict['country_id_count'] or '0'"/> <t t-esc="country_dict['country_id'][1]"/> </a> @@ -107,20 +127,20 @@ </t> </t> </ul> - </xpath> </template> -<template id="opt_tag_list" inherit_id="website_customer.index" customize_show="True" name="Filter on Tags" priority="5"> + +<template id="opt_tag_list" inherit_id="website_customer.index" customize_show="True" name="Filter on Tags" priority="40"> <xpath expr="//div[@id='ref_left_column']" position="inside"> <h3 t-if="len(tags)">References by Tag</h3> <ul class="nav nav-pills nav-stacked mt16 mb32" t-if="len(tags)"> - <a class="mr8" t-attf-href="/customers/#{ current_country and 'country/%s' % slug(current_country) or '' }#{ search_path }"> + <a class="mr8" t-attf-href="/customers/#{ current_industry_id and 'industry/%s/' % slug(current_industry) or '' }#{ current_country_id and 'country/%s' % slug(current_country) or '' }"> <span class="fa fa-1x fa-tags"/> All </a> <t t-foreach="tags" t-as="o_tag"> <a t-attf-class="label label-#{o_tag.classname}" t-esc="o_tag.name" t-att-style="tag and tag.id==o_tag.id and 'text-decoration: underline'" - t-attf-href="/customers/#{ 'tag/%s/' % slug(o_tag) }#{ current_country and 'country/%s' % slug(current_country) or '' }#{ search_path }"/> + t-attf-href="/customers/#{ current_industry_id and 'industry/%s/' % slug(current_industry) or '' }#{ current_country_id and 'country/%s' % slug(current_country) or '' }?tag_id=#{slug(o_tag)}"/> </t> </ul> </xpath> @@ -160,6 +180,14 @@ </xpath> </template> +<template id="partner_detail" inherit_id="website_partner.partner_detail" name="Partner Details"> + <xpath expr="//address" position="inside"> + <t t-if="partner.industry_id"> + <span class="label label-default"><t t-esc="partner.industry_id.name"/></span> + </t> + </xpath> +</template> + <template id="implemented_by_block" name="Partner Implemented By Block"> <t t-if="partner.assigned_partner_id and partner.assigned_partner_id.website_published"> <div class="panel panel-default"> @@ -201,6 +229,9 @@ <a class="media-heading" t-attf-href="/customers/#{slug(reference)}"> <span t-field="reference.self"/> </a> + <t t-if="reference.industry_id"> + <span class="label label-default"><t t-esc="reference.industry_id.name"/></span> + </t> <div t-field='reference.website_short_description'/> </div> </t> diff --git a/addons/website_form/static/src/js/website_form.js b/addons/website_form/static/src/js/website_form.js index eeecbea9389..e95f37ef335 100644 --- a/addons/website_form/static/src/js/website_form.js +++ b/addons/website_form/static/src/js/website_form.js @@ -167,13 +167,15 @@ odoo.define('website_form.animation', function (require) { // Special cases for dates and datetimes } else if ($(input).hasClass('o_website_form_date')) { - return !self.is_datetime_valid(input.value, 'date'); + if (!self.is_datetime_valid(input.value, 'date')) { + return true; + } } else if ($(input).hasClass('o_website_form_datetime')) { - return !self.is_datetime_valid(input.value, 'datetime'); - - } else { - return !input.checkValidity(); + if (!self.is_datetime_valid(input.value, 'datetime')) { + return true; + } } + return !input.checkValidity(); }) // Update field color if invalid or erroneous diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index c6fc592e3d8..546def6890a 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -34,6 +34,7 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_ else: if not author_id: raise NotFound() + kw.pop('csrf_token', None) return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message, message_type=kw.pop('message_type', "comment"), subtype=kw.pop('subtype', "mt_comment"), diff --git a/addons/website_portal/wizard/portal_wizard.py b/addons/website_portal/wizard/portal_wizard.py index 2da830ef9d6..4bc731c3ff8 100644 --- a/addons/website_portal/wizard/portal_wizard.py +++ b/addons/website_portal/wizard/portal_wizard.py @@ -127,7 +127,7 @@ class PortalWizardUser(models.TransientModel): for wizard_user in self.sudo().with_context(active_test=False): group_portal = wizard_user.wizard_id.portal_id if not group_portal.is_portal: - raise UserError('Not a portal: ' + group_portal.name) + raise UserError(_('Group %s is not a portal') % group_portal.name) user = wizard_user.partner_id.user_ids[0] if wizard_user.partner_id.user_ids else None # update partner email, if a new one was introduced if wizard_user.partner_id.email != wizard_user.email: diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py index 42d95d458b1..86a56b0509b 100644 --- a/addons/website_portal_sale/controllers/main.py +++ b/addons/website_portal_sale/controllers/main.py @@ -238,10 +238,10 @@ class website_account(website_account): partner = request.env['res.users'].browse(request.uid).partner_id invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])]) if invoices: - if (data.get('vat', partner.vat) or False) != partner.vat: + if 'vat' in partner and (data['vat'] or False) != (partner.vat or False): error['vat'] = 'error' error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) - if data.get('name', partner.name) != partner.name: + if 'name' in data and (data['name'] or False) != (partner.name or False): error['name'] = 'error' error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) return error, error_message diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index c7d09e139c8..f441087de33 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -73,6 +73,13 @@ class SaleOrder(models.Model): (2, 'Immediate after website order validation and save a token'), ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote") + @api.multi + def copy(self, default=None): + if self.template_id and self.template_id.number_of_days > 0: + default = dict(default or {}) + default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days)) + return super(SaleOrder, self).copy(default=default) + @api.one def _compute_amount_undiscounted(self): total = 0.0 diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index c43e3353671..8af8e148a9e 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -322,7 +322,8 @@ class WebsiteSale(http.Controller): @http.route(['/shop/change_pricelist/<model("product.pricelist"):pl_id>'], type='http', auth="public", website=True) def pricelist_change(self, pl_id, **post): - if request.website.is_pricelist_available(pl_id.id): + if (pl_id.selectable or pl_id == request.env.user.partner_id.property_product_pricelist) \ + and request.website.is_pricelist_available(pl_id.id): request.session['website_sale_current_pl'] = pl_id.id request.website.sale_get_order(force_pricelist=pl_id.id) return request.redirect(request.httprequest.referrer or '/shop') diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 0a379588071..fe22160267f 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -21,7 +21,7 @@ class ProductPricelist(models.Model): return self.env['website'].search([], limit=1) website_id = fields.Many2one('website', string="website", default=_default_website) - code = fields.Char(string='E-commerce Promotional Code') + code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user") selectable = fields.Boolean(help="Allow the end user to choose this price list") def clear_cache(self): diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 355af951f61..a933dd3574d 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -61,7 +61,7 @@ class SaleOrder(models.Model): if order.pricelist_id and order.partner_id: order_line = order._cart_find_product_line(product.id) if order_line: - pu = self.env['account.tax']._fix_tax_included_price(pu, product.taxes_id, order_line.tax_id) + pu = self.env['account.tax']._fix_tax_included_price(pu, product.taxes_id, order_line[0].tax_id) return { 'product_id': product_id, @@ -218,7 +218,7 @@ class Website(models.Model): if not pricelists: # no pricelist for this country, or no GeoIP pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl)) else: - pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.code) + pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code) # This method is cached, must not return records! See also #8795 return pricelists.ids @@ -387,6 +387,9 @@ class Website(models.Model): partner.write({'last_website_so_id': sale_order.id}) if sale_order: + # case when user emptied the cart + if not request.session.get('sale_order_id'): + request.session['sale_order_id'] = sale_order.id # check for change of pricelist with a coupon pricelist_id = pricelist_id or partner.property_product_pricelist.id @@ -425,7 +428,7 @@ class Website(models.Model): update_pricelist = True if code and code != sale_order.pricelist_id.code: - code_pricelist = self.env['product.pricelist'].search([('code', '=', code)], limit=1) + code_pricelist = self.env['product.pricelist'].sudo().search([('code', '=', code)], limit=1) if code_pricelist: pricelist_id = code_pricelist.id update_pricelist = True diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 80958f3fb70..8bf5fdf3ff0 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -605,7 +605,7 @@ <span itemprop="price" style="display:none;" t-esc="product.website_price"/> <span itemprop="priceCurrency" style="display:none;" t-esc="website.currency_id.name"/> </h4> - <h4 class="css_non_editable_mode_hidden decimal_precision" t-att-data-precision="product.currency_id.decimal_places"> + <h4 class="css_non_editable_mode_hidden decimal_precision" t-att-data-precision="str(product.currency_id.decimal_places)"> <span t-field="product.lst_price" t-options='{ "widget": "monetary", |
