summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorOlivier Dony <odo@openerp.com>2017-06-01 01:46:13 +0200
committerOlivier Dony <odo@openerp.com>2017-06-01 01:46:13 +0200
commit5d2869cbc812f716e3115b5efb9bd1c3082f47b7 (patch)
tree364844fc326f0d095997120d10d83e67fdf341e9 /addons
parent04111cae39e36015d3c8494f52b110ab5ab6ad80 (diff)
parentba15df47cb25625cb0921cba554aa0879048c90a (diff)
[MERGE] Forward-port saas-16 up to ba15df47cb25625cb0921cba554aa0879048c90a
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account.py6
-rw-r--r--addons/account/models/account_bank_statement.py2
-rw-r--r--addons/account/models/account_invoice.py33
-rw-r--r--addons/account/models/account_move.py4
-rw-r--r--addons/account/models/account_payment.py14
-rw-r--r--addons/account/views/account_invoice_view.xml2
-rw-r--r--addons/account/views/account_view.xml3
-rw-r--r--addons/account/wizard/account_invoice_refund.py5
-rw-r--r--addons/account_asset/static/tests/account_asset_tests.js2
-rw-r--r--addons/account_bank_statement_import/account_bank_statement_import.py5
-rw-r--r--addons/account_bank_statement_import/i18n/account_bank_statement_import.pot6
-rw-r--r--addons/account_check_printing/models/account_payment.py24
-rw-r--r--addons/analytic/views/analytic_account_views.xml2
-rw-r--r--addons/barcodes/models/barcodes.py4
-rw-r--r--addons/barcodes/tests/__init__.py1
-rw-r--r--addons/barcodes/tests/test_barcode_nomenclature.py15
-rw-r--r--addons/base_vat/models/res_partner.py8
-rw-r--r--addons/board/static/src/js/dashboard.js33
-rw-r--r--addons/board/static/src/less/dashboard.less4
-rw-r--r--addons/board/static/src/xml/board.xml5
-rw-r--r--addons/board/static/tests/dashboard_tests.js51
-rw-r--r--addons/calendar/models/mail_message.py2
-rw-r--r--addons/crm/models/crm_lead.py43
-rw-r--r--addons/crm/models/res_config.py5
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity.py7
-rw-r--r--addons/event_sale/models/sale_order.py3
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml1
-rw-r--r--addons/hr_expense/wizard/hr_expense_register_payment.py2
-rw-r--r--addons/hr_expense_check/__init__.py1
-rw-r--r--addons/hr_expense_check/__manifest__.py16
-rw-r--r--addons/hr_expense_check/models/__init__.py1
-rw-r--r--addons/hr_expense_check/models/payment.py39
-rw-r--r--addons/hr_expense_check/views/payment.xml18
-rw-r--r--addons/hr_holidays/controllers/main.py2
-rw-r--r--addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py2
-rw-r--r--addons/l10n_be_invoice_bba/i18n/l10n_be_invoice_bba.pot2
-rw-r--r--addons/l10n_be_invoice_bba/models/account_invoice.py368
-rw-r--r--addons/l10n_cn/__manifest__.py1
-rw-r--r--addons/l10n_cn_small_business/__manifest__.py1
-rw-r--r--addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml6
-rw-r--r--addons/l10n_cn_standard/__manifest__.py1
-rw-r--r--addons/l10n_do/__manifest__.py2
-rw-r--r--addons/l10n_do/data/account.account.template.csv525
-rw-r--r--addons/l10n_do/data/account.tax.template.xml2
-rw-r--r--addons/l10n_do/data/account_account_tag_data.xml555
-rw-r--r--addons/l10n_do/data/account_chart_template_data.xml8
-rw-r--r--addons/l10n_do/data/l10n_do_chart_data.xml1
-rw-r--r--addons/l10n_mx/data/account_tax_data.xml16
-rw-r--r--addons/l10n_mx/data/fiscal_position_data.xml100
-rw-r--r--addons/l10n_mx/models/chart_template.py38
-rw-r--r--addons/mail/models/res_partner.py3
-rw-r--r--addons/maintenance/views/maintenance_views.xml2
-rw-r--r--addons/marketing_campaign/models/marketing_campaign.py2
-rw-r--r--addons/mass_mailing/data/mass_mailing_data.xml12
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/mass_mailing/static/src/less/themes/theme_default.less7
-rw-r--r--addons/mass_mailing/views/mass_mailing_views.xml13
-rw-r--r--addons/membership/wizard/membership_invoice.py3
-rw-r--r--addons/mrp/models/mrp_unbuild.py5
-rw-r--r--addons/mrp/models/stock_move.py2
-rw-r--r--addons/mrp/report/mrp_production_templates.xml3
-rw-r--r--addons/mrp/static/src/xml/mrp.xml6
-rw-r--r--addons/mrp/static/tests/mrp_tests.js4
-rw-r--r--addons/mrp/views/mrp_bom_views.xml2
-rw-r--r--addons/payment/models/account_payment.py4
-rw-r--r--addons/payment/models/payment_acquirer.py6
-rw-r--r--addons/payment_authorize/i18n/payment_authorize.pot19
-rw-r--r--addons/payment_authorize/models/authorize_request.py2
-rw-r--r--addons/payment_authorize/models/payment.py4
-rw-r--r--addons/payment_paypal/views/payment_paypal_templates.xml2
-rw-r--r--addons/payment_sips/__manifest__.py22
-rw-r--r--addons/payment_sips/controllers/main.py2
-rw-r--r--addons/payment_sips/models/payment.py2
-rw-r--r--addons/point_of_sale/i18n/point_of_sale.pot6
-rw-r--r--addons/point_of_sale/models/pos_order.py3
-rw-r--r--addons/point_of_sale/models/pos_session.py6
-rw-r--r--addons/point_of_sale/static/src/js/screens.js1
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml2
-rw-r--r--addons/point_of_sale/wizard/pos_payment.xml2
-rw-r--r--addons/portal/i18n/portal.pot6
-rw-r--r--addons/pos_mercury/i18n/pos_mercury.pot26
-rw-r--r--addons/pos_mercury/models/pos_mercury_transaction.py5
-rw-r--r--addons/pos_mercury/static/src/js/pos_mercury.js4
-rw-r--r--addons/product/models/product_attribute.py2
-rw-r--r--addons/product/models/product_uom.py2
-rw-r--r--addons/product/views/product_views.xml4
-rw-r--r--addons/product_margin/views/product_product_views.xml5
-rw-r--r--addons/project/views/project_views.xml2
-rw-r--r--addons/purchase/models/account_invoice.py8
-rw-r--r--addons/purchase/models/purchase.py33
-rw-r--r--addons/purchase/views/purchase_views.xml2
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py2
-rw-r--r--addons/sale/models/account_invoice.py3
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/tests/test_sale_order.py2
-rw-r--r--addons/sale_stock/models/stock.py2
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_templates.xml6
-rw-r--r--addons/sales_team/views/crm_team_views.xml2
-rw-r--r--addons/stock/models/procurement.py2
-rw-r--r--addons/stock/models/product.py6
-rw-r--r--addons/stock/models/stock_incoterms.py2
-rw-r--r--addons/stock/report/report_deliveryslip.xml3
-rw-r--r--addons/stock/views/product_views.xml11
-rw-r--r--addons/stock/views/stock_quant_views.xml2
-rw-r--r--addons/stock_account/models/account_invoice.py4
-rw-r--r--addons/stock_account/wizard/wizard_valuation_history_views.xml1
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js4
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js28
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js15
-rw-r--r--addons/web/static/src/js/core/mixins.js1
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js11
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js41
-rw-r--r--addons/web/static/src/js/fields/field_utils.js70
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js124
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js78
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js399
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js24
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js27
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js2
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js2
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js59
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js25
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js6
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js19
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js20
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js127
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js64
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js15
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js27
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js6
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js8
-rw-r--r--addons/web/static/src/js/widgets/data_export.js5
-rw-r--r--addons/web/static/src/less/form_view.less1
-rw-r--r--addons/web/static/src/less/layout_background.less2
-rw-r--r--addons/web/static/src/less/list_view.less21
-rw-r--r--addons/web/static/src/xml/base.xml16
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js156
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js12
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js890
-rw-r--r--addons/web/static/tests/helpers/mock_server.js127
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js109
-rw-r--r--addons/web/static/tests/views/form_tests.js409
-rw-r--r--addons/web/static/tests/views/kanban_model_tests.js49
-rw-r--r--addons/web/static/tests/views/kanban_tests.js16
-rw-r--r--addons/web/static/tests/views/list_benchmarks.js19
-rw-r--r--addons/web/static/tests/views/list_tests.js137
-rw-r--r--addons/web/static/tests/views/pivot_tests.js186
-rw-r--r--addons/web/static/tests/widgets/data_export_tests.js132
-rw-r--r--addons/web/views/webclient_templates.xml1
-rw-r--r--addons/web_editor/static/src/js/backend.js25
-rw-r--r--addons/web_editor/static/src/js/rte.summernote.js7
-rw-r--r--addons/web_editor/static/src/js/snippets.editor.js22
-rw-r--r--addons/web_editor/static/src/js/transcoder.js10
-rw-r--r--addons/web_editor/static/tests/web_editor_tests.js27
-rw-r--r--addons/website/models/ir_http.py4
-rw-r--r--addons/website/static/src/js/website.seo.js3
-rw-r--r--addons/website/static/src/less/website.snippets.less1
-rw-r--r--addons/website/static/src/less/website.ui.less6
-rw-r--r--addons/website_blog/static/src/js/contentshare.js9
-rw-r--r--addons/website_blog/static/src/js/website_blog.js2
-rw-r--r--addons/website_customer/controllers/main.py75
-rw-r--r--addons/website_customer/views/website_customer_templates.xml43
-rw-r--r--addons/website_form/static/src/js/website_form.js12
-rw-r--r--addons/website_mail/controllers/main.py1
-rw-r--r--addons/website_portal/wizard/portal_wizard.py2
-rw-r--r--addons/website_portal_sale/controllers/main.py4
-rw-r--r--addons/website_quote/models/sale_order.py7
-rw-r--r--addons/website_sale/controllers/main.py3
-rw-r--r--addons/website_sale/models/product.py2
-rw-r--r--addons/website_sale/models/sale_order.py9
-rw-r--r--addons/website_sale/views/templates.xml2
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 &amp; 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 &amp; 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', '&gt;', 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', '&gt;', 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 + "&shy;" + 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&nbsp;€",
+ "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 &lt; 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",